@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.
- package/CHANGELOG.md +81 -0
- package/PROMOTED.md +46 -0
- package/README.md +28 -19
- package/bin/cli.mjs +87 -216
- package/bin/lib/cli-shared.mjs +131 -0
- package/bin/lib/copy.mjs +276 -6
- 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/code-style.md +4 -1
- package/docs/ownership-vs-shadcn-lock.md +2 -3
- package/docs/protocol.md +7 -7
- package/docs/releasing.md +8 -2
- package/package.json +7 -3
- package/registry/instructions/opus.md +3 -3
- package/registry/templates/app/package.json +1 -1
- package/registry/templates/app/src/App.tsx +11 -6
- 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 +5 -6
- 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/action-list-dialog.tsx +10 -3
- package/src/ui/components/patterns/confirm.tsx +2 -31
- package/src/ui/components/patterns/content-header.tsx +44 -137
- package/src/ui/components/patterns/data-state.tsx +42 -68
- package/src/ui/components/patterns/dock.tsx +20 -3
- package/src/ui/components/patterns/form.tsx +26 -22
- package/src/ui/components/patterns/list.tsx +18 -12
- package/src/ui/components/patterns/page-state.tsx +39 -51
- package/src/ui/components/patterns/page.tsx +37 -54
- package/src/ui/components/patterns/sidebar.tsx +17 -6
- package/src/ui/components/patterns/state-surface.tsx +148 -0
- package/src/ui/components/patterns/surface-header.tsx +119 -0
- package/src/ui/components/patterns/trigger.tsx +21 -25
- package/src/ui/components/patterns/view.tsx +29 -22
- package/src/ui/components/primitives/alert.tsx +1 -27
- package/src/ui/components/primitives/ask.tsx +3 -3
- package/src/ui/components/primitives/avatar.tsx +15 -5
- package/src/ui/components/primitives/badge.tsx +5 -41
- package/src/ui/components/primitives/breadcrumb.tsx +2 -2
- package/src/ui/components/primitives/button.tsx +39 -30
- 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 +72 -0
- package/src/ui/components/primitives/copyable.tsx +1 -1
- package/src/ui/components/primitives/dialog.tsx +12 -7
- package/src/ui/components/primitives/dot.tsx +1 -25
- package/src/ui/components/primitives/drawer.tsx +10 -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 +12 -9
- 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 -1
- package/src/ui/components/primitives/menu.tsx +1 -7
- package/src/ui/components/primitives/pagination.tsx +16 -8
- 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 +6 -6
- package/src/ui/components/primitives/slider.tsx +5 -1
- package/src/ui/components/primitives/sonner.tsx +3 -0
- package/src/ui/components/primitives/spinner.tsx +13 -16
- package/src/ui/components/primitives/switch.tsx +5 -1
- package/src/ui/components/primitives/tabs.tsx +6 -3
- package/src/ui/components/primitives/textarea.tsx +1 -1
- package/src/ui/components/primitives/toggle.tsx +9 -4
- package/src/ui/components/primitives/tooltip.tsx +1 -0
- package/src/ui/docs/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +36 -3
- package/src/ui/docs/content/action-list-dialog.md +2 -2
- package/src/ui/docs/content/action-list.md +13 -2
- package/src/ui/docs/content/action-trigger.md +12 -5
- package/src/ui/docs/content/action-view.md +11 -3
- package/src/ui/docs/content/ask.md +11 -0
- package/src/ui/docs/content/avatar.md +7 -3
- package/src/ui/docs/content/button.md +30 -14
- package/src/ui/docs/content/calendar.md +13 -0
- package/src/ui/docs/content/card.md +26 -0
- package/src/ui/docs/content/chat.md +20 -0
- package/src/ui/docs/content/cli.md +71 -19
- package/src/ui/docs/content/communication.md +36 -0
- package/src/ui/docs/content/composer.md +15 -0
- package/src/ui/docs/content/content.md +17 -1
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/data-state.md +17 -13
- package/src/ui/docs/content/detail.md +19 -1
- package/src/ui/docs/content/dialog.md +1 -4
- package/src/ui/docs/content/dictionary-value.md +9 -2
- package/src/ui/docs/content/dock.md +8 -0
- package/src/ui/docs/content/dot.md +8 -0
- package/src/ui/docs/content/empty.md +2 -2
- package/src/ui/docs/content/getting-started.md +2 -2
- package/src/ui/docs/content/icon-picker.md +11 -0
- package/src/ui/docs/content/input.md +1 -1
- package/src/ui/docs/content/item.md +1 -1
- package/src/ui/docs/content/label.md +7 -0
- package/src/ui/docs/content/menu.md +6 -0
- package/src/ui/docs/content/metric-card.md +13 -0
- package/src/ui/docs/content/page.md +20 -4
- package/src/ui/docs/content/pagination.md +11 -9
- package/src/ui/docs/content/popover.md +6 -0
- package/src/ui/docs/content/progress.md +8 -11
- package/src/ui/docs/content/select.md +5 -5
- package/src/ui/docs/content/semantic-context.md +5 -4
- package/src/ui/docs/content/sidebar.md +13 -47
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/spinner.md +9 -6
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/switch.md +1 -1
- package/src/ui/docs/content/tabs.md +1 -1
- package/src/ui/docs/content/textarea.md +7 -0
- package/src/ui/docs/content/toggle.md +1 -1
- package/src/ui/docs/content/tokens.md +4 -4
- package/src/ui/docs/content/truncate.md +8 -0
- package/src/ui/docs/content/ui.md +14 -0
- package/src/ui/docs/doc-client.tsx +5 -5
- package/src/ui/docs/doc.tsx +26 -14
- package/src/ui/docs/registry.tsx +5 -5
- package/src/ui/docs/standalone.tsx +2 -2
- package/src/ui/drivers/react.tsx +12 -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 +8 -8
- package/src/ui/react.tsx +10 -16
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
- package/src/ui/components/patterns/shell-nav.tsx +0 -147
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
|
@@ -221,13 +221,13 @@ export const PROTOCOL_SECTIONS: DocSection[] = [
|
|
|
221
221
|
pages: [
|
|
222
222
|
{
|
|
223
223
|
slug: "getting-started",
|
|
224
|
-
title: "
|
|
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
|
|
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: "
|
|
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
|
|
352
|
+
{ slug: "tokens", title: "Tokens e tema", render: doc(tokensMd) },
|
|
353
353
|
{
|
|
354
354
|
slug: "semantic-context",
|
|
355
|
-
title: "Contexto
|
|
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 `
|
|
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
|
|
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,20 @@ 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
86
|
function useClient(): ClientAdapter {
|
|
87
|
-
const ctx = useContext(
|
|
87
|
+
const ctx = useContext(OpusContext)
|
|
88
88
|
if (ctx === null) {
|
|
89
89
|
throw new Error(
|
|
90
|
-
'useAction/useLookupAction
|
|
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(
|
|
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
|
+
}
|
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
|
}
|
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,
|
|
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
|
|
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: "
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
111
|
-
que
|
|
112
|
-
|
|
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
|
package/src/vite/design.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* // vite --mode design
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readdirSync
|
|
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 =
|
|
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: `
|
|
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: `
|
|
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: `
|
|
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
|
-
}
|