@softize/opus 13.0.0 → 13.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -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/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/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/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 +2 -2
- 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 +4 -4
- 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 +17 -6
- package/src/ui/components/patterns/trigger.tsx +14 -16
- package/src/ui/components/patterns/view.tsx +26 -17
- package/src/ui/components/primitives/alert.tsx +11 -5
- 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 +12 -7
- package/src/ui/components/primitives/dot.tsx +5 -0
- 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 +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/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 +3 -0
- 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/changelog.tsx +1 -1
- package/src/ui/docs/content/action-form.md +10 -3
- package/src/ui/docs/content/action-list.md +10 -1
- package/src/ui/docs/content/action-trigger.md +8 -1
- package/src/ui/docs/content/action-view.md +9 -2
- package/src/ui/docs/content/ask.md +11 -0
- 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/composer.md +15 -0
- package/src/ui/docs/content/content.md +15 -0
- package/src/ui/docs/content/copyable.md +8 -0
- package/src/ui/docs/content/detail.md +19 -1
- 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/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 +8 -0
- 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 +2 -2
- package/src/ui/docs/content/sidebar.md +14 -8
- package/src/ui/docs/content/skeleton.md +6 -0
- package/src/ui/docs/content/split.md +21 -0
- package/src/ui/docs/content/textarea.md +7 -0
- 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 +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 +3 -3
- package/src/ui/react.tsx +2 -0
- package/src/ui/theme.css +10 -8
- package/src/vite/design.ts +6 -18
|
@@ -134,8 +134,9 @@ render(
|
|
|
134
134
|
Esse é o padrão usado pelo `DocBrowser`: seções são rótulos, grupos nomeados são nós expansíveis e
|
|
135
135
|
páginas são folhas. Um grupo começa aberto e volta a abrir quando contém a página ativa.
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
rodap
|
|
137
|
+
`ShellNav` continua disponível para uma navegação plana com cabeçalho próprio e grupos ancorados no
|
|
138
|
+
rodapé, mas está descontinuado: código novo usa `SidebarNav`, que cobre o mesmo caso integrado à
|
|
139
|
+
`Sidebar`. Ele gerencia a rolagem internamente e se adapta quando estiver dentro de uma `Sidebar`
|
|
139
140
|
recolhida.
|
|
140
141
|
|
|
141
142
|
```tsx
|
|
@@ -156,6 +157,10 @@ recolhida.
|
|
|
156
157
|
somente os ícones e expõem os rótulos em tooltips. Por isso, todo destino que aparece no modo
|
|
157
158
|
recolhido precisa de um ícone reconhecível e de um `label` completo.
|
|
158
159
|
|
|
160
|
+
O slot de ícone do `SidebarItem` ocupa `1rem` nos dois estados e normaliza SVGs para essa medida.
|
|
161
|
+
O consumidor escolhe o símbolo e sua cor sem precisar repetir largura ou altura em ícones SVG.
|
|
162
|
+
Outros tipos de `ReactNode` continuam responsáveis pelas próprias dimensões.
|
|
163
|
+
|
|
159
164
|
```tsx preview
|
|
160
165
|
const [collapsed, setCollapsed] = useState(false)
|
|
161
166
|
|
|
@@ -216,12 +221,13 @@ estrutura evita botões aninhados e permite que cada controle receba foco de for
|
|
|
216
221
|
}
|
|
217
222
|
onClick={() => {}}
|
|
218
223
|
/>
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
<SidebarTreeGroup>
|
|
225
|
+
<SidebarItem
|
|
226
|
+
label="Criar workspace"
|
|
227
|
+
icon={<FilePlus2 />}
|
|
228
|
+
onClick={() => {}}
|
|
229
|
+
/>
|
|
230
|
+
</SidebarTreeGroup>
|
|
225
231
|
</div>
|
|
226
232
|
```
|
|
227
233
|
|
|
@@ -33,3 +33,9 @@ O esqueleto reproduz o layout final do card — título, descrição, conteúdo
|
|
|
33
33
|
</CardFooter>
|
|
34
34
|
</Card>
|
|
35
35
|
```
|
|
36
|
+
|
|
37
|
+
## Propriedades de Skeleton
|
|
38
|
+
|
|
39
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| `className` | `string` | | Dá forma ao placeholder (`h-4 w-40`, `size-10 rounded-full`); o componente só traz o pulso e o fundo. |
|
|
@@ -74,3 +74,24 @@ const defaultLayout = readLayout('workspace-layout')
|
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
Em aplicações renderizadas no servidor, leia o armazenamento somente no cliente. `onLayoutChanged` também permite usar `sessionStorage` ou uma camada própria quando o layout precisa acompanhar outro escopo.
|
|
77
|
+
|
|
78
|
+
## Propriedades de Split
|
|
79
|
+
|
|
80
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Sentido em que os panes se alinham. |
|
|
83
|
+
| `resizable` | `boolean` | `false` | Cada fronteira ganha um separador acessível e arrastável. |
|
|
84
|
+
| `handle` | `boolean` | `false` | Mostra a alça visual no separador. |
|
|
85
|
+
| `id` | `string` | | Identidade estável do grupo redimensionável. |
|
|
86
|
+
| `defaultLayout` | `Record<string, number>` | | Layout percentual restaurado, indexado pelos ids dos panes. |
|
|
87
|
+
| `onLayoutChanged` | `(layout, { isUserInteraction }) => void` | | Chamado ao concluir uma mudança de layout; persista onde fizer sentido. |
|
|
88
|
+
|
|
89
|
+
## Propriedades de Pane
|
|
90
|
+
|
|
91
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| `id` | `string` | | Identidade estável usada pelo layout redimensionável e persistido. |
|
|
94
|
+
| `initialSize` | `number \| string` | | Tamanho inicial; número é porcentagem, string aceita `%`, `rem`, `em`, `vh`, `vw` e `px`. |
|
|
95
|
+
| `minSize` / `maxSize` | `number \| string` | | Limites quando o split é redimensionável. |
|
|
96
|
+
| `grow` | `boolean` | `false` | Ocupa o espaço remanescente no layout simples. |
|
|
97
|
+
| `inset` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Respiro interno; `none` para chrome, navegação ou conteúdo com inset próprio. |
|
|
@@ -29,3 +29,10 @@ Associe `htmlFor` no `Label` ao `id` do campo para manter o rótulo acessível.
|
|
|
29
29
|
<Textarea aria-invalid placeholder="Conte o contexto da mudança." />
|
|
30
30
|
<Textarea disabled defaultValue="Sessão encerrada — o brief não pode mais ser editado." />
|
|
31
31
|
```
|
|
32
|
+
|
|
33
|
+
## Propriedades de Textarea
|
|
34
|
+
|
|
35
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `…props` | `ComponentProps<'textarea'>` | | Todos os atributos nativos (`value`, `defaultValue`, `rows`, `disabled`, `aria-invalid`…). A altura acompanha o conteúdo (`field-sizing-content`) a partir de `min-h-16`. |
|
|
38
|
+
| `className` | `string` | | Compõe sobre o estilo padrão. |
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Tokens
|
|
2
|
+
title: Tokens e tema
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
# Tokens
|
|
5
|
+
# Tokens e tema
|
|
6
6
|
|
|
7
7
|
O tema canônico vive no Opus (`theme.css`): tokens com a cor inteira na var, mapeados para o
|
|
8
8
|
Tailwind via `@theme inline`. Os swatches abaixo leem as vars **ao vivo** — troque o tema do app
|
|
@@ -73,8 +73,8 @@ render(
|
|
|
73
73
|
|
|
74
74
|
## Linhas e foco
|
|
75
75
|
|
|
76
|
-
> Bordas e o anel de foco também são tokens — nada de cinza hardcoded.
|
|
77
|
-
>
|
|
76
|
+
> Bordas e o anel de foco também são tokens — nada de cinza hardcoded. Superfícies elevadas usam
|
|
77
|
+
> a mesma `border-border`; o que muda entre elas é a sombra, descrita na seção Escala de elevação.
|
|
78
78
|
|
|
79
79
|
```tsx preview
|
|
80
80
|
const LINES = [
|
|
@@ -60,3 +60,11 @@ quando o texto completo ainda não oferecer contexto suficiente.
|
|
|
60
60
|
|
|
61
61
|
Requer `TooltipProvider` na raiz (o esqueleto do `opus create` já monta). Largura vem do
|
|
62
62
|
container ou de `className` (`max-w-*`) — o span é `block truncate`.
|
|
63
|
+
|
|
64
|
+
## Propriedades de Truncate
|
|
65
|
+
|
|
66
|
+
| Propriedade | Tipo | Padrão | Descrição |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `tooltip` | `ReactNode` | os próprios `children` | Conteúdo da dica quando o texto transborda. |
|
|
69
|
+
| `fade` | `boolean` | `false` | Sinaliza o corte esmaecendo o fim da linha, no lugar das reticências. |
|
|
70
|
+
| `className` | `string` | | Largura (`max-w-*`) e demais ajustes; o span é `block truncate`. |
|
|
@@ -30,6 +30,20 @@ import { Button, Dialog, useAction } from '@softize/opus/ui/react'
|
|
|
30
30
|
@source '../node_modules/@softize/opus/src/ui';
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Prebundle do Vite
|
|
34
|
+
|
|
35
|
+
A entrada `@softize/opus/ui/react` é um `.tsx`, e o prebundle do Vite só considera entradas
|
|
36
|
+
`.js`/`.ts`. Sem ajuste, o dev server avisa `Cannot optimize dependency` e serve a árvore do Opus
|
|
37
|
+
arquivo a arquivo na carga fria. Há duas saídas, cada uma com um custo conhecido:
|
|
38
|
+
|
|
39
|
+
- `optimizeDeps.include: ['@softize/opus/ui/react']` com `extensions: ['.tsx']` pré-empacota o
|
|
40
|
+
Opus, mas pode duplicar `@tanstack/react-query` quando o app também o importa direto (dois
|
|
41
|
+
`QueryClient`, hooks fora do provider).
|
|
42
|
+
- `optimizeDeps.exclude: ['@softize/opus/ui/react']` evita a duplicação e infla a carga fria.
|
|
43
|
+
|
|
44
|
+
Escolha pelo sintoma que aparece no seu app e registre a decisão no `vite.config.ts`. A saída
|
|
45
|
+
definitiva (entrada `.ts` no pacote) está no radar do Opus.
|
|
46
|
+
|
|
33
47
|
## Origem e anti-drift
|
|
34
48
|
|
|
35
49
|
> Cada componente declara de onde veio — e a regra de sincronização vem junto.
|
|
@@ -8,7 +8,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
|
8
8
|
import { z } from 'zod'
|
|
9
9
|
import { attachLogicalType, defineContract, error } from '../../core/index.ts'
|
|
10
10
|
import type { ActionDef, ActionResult, ClientAdapter, LogicalTypeMeta, Paginated } from '../../core/index.ts'
|
|
11
|
-
import {
|
|
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
|
@@ -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 (`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
|
}
|
package/src/ui/meta.ts
CHANGED
|
@@ -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",
|
package/src/ui/react.tsx
CHANGED
|
@@ -426,6 +426,7 @@ export {
|
|
|
426
426
|
PageDescription,
|
|
427
427
|
PageMeta,
|
|
428
428
|
PageActions,
|
|
429
|
+
PageActionsTarget,
|
|
429
430
|
PageBody,
|
|
430
431
|
} from "./components/patterns/page.tsx";
|
|
431
432
|
export type { PageProps } from "./components/patterns/page.tsx";
|
|
@@ -475,6 +476,7 @@ export type {
|
|
|
475
476
|
DockProps,
|
|
476
477
|
DockActionProps,
|
|
477
478
|
SurfaceStatusProps,
|
|
479
|
+
SurfaceStatusContext,
|
|
478
480
|
} from "./components/patterns/dock.tsx";
|
|
479
481
|
|
|
480
482
|
// Barra lateral composicional: header/conteúdo/footer e navegação, sem possuir o layout.
|
package/src/ui/theme.css
CHANGED
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
--context-primary-subtle: color-mix(in oklab, var(--primary) 8%, transparent);
|
|
60
60
|
--context-primary-emphasis: var(--foreground);
|
|
61
61
|
--context-primary-border: color-mix(in oklab, var(--primary) 25%, transparent);
|
|
62
|
+
/* info/success/warning: base, foreground, subtle e border (12 tokens) valem nos dois temas
|
|
63
|
+
de propósito — são cores saturadas com alpha, que já se assentam sobre qualquer canvas;
|
|
64
|
+
só `*-emphasis` (texto sobre o popover) precisa clarear no escuro, e é redefinido em .dark. */
|
|
62
65
|
--context-info: hsl(217 91% 60%);
|
|
63
66
|
--context-info-foreground: hsl(0 0% 100%);
|
|
64
67
|
--context-info-subtle: hsl(217 91% 60% / 0.1);
|
|
@@ -82,7 +85,7 @@
|
|
|
82
85
|
--border: hsl(0 0% 92.5%);
|
|
83
86
|
--input: hsl(0 0% 92.5%);
|
|
84
87
|
/* Ring de foco LEVE (pedido da casa): aro sutil, não um halo. Com ring-[0.1875rem] + /50,
|
|
85
|
-
~86% encosta na borda (
|
|
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
|