@softize/opus 8.6.6
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 +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Actions & contratos
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Actions & contratos
|
|
6
|
+
|
|
7
|
+
A action é a unidade do Opus. O contrato (entidade + input/output + metadados) mora no package
|
|
8
|
+
compartilhado; a API amarra o handler; a web renderiza a partir do mesmo contrato. Um shape,
|
|
9
|
+
três consumidores.
|
|
10
|
+
|
|
11
|
+
## Contrato primeiro
|
|
12
|
+
|
|
13
|
+
> `defineContract` no `shared/` — entidades, schemas (zod + `t.*` pra tipos lógicos) e os
|
|
14
|
+
> metadados da action. `api` e `web` importam; nada se duplica.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
// shared/ — roda nos dois lados.
|
|
18
|
+
import { defineContract } from '@softize/opus'
|
|
19
|
+
import { t } from '@softize/opus/schema/zod'
|
|
20
|
+
import { z } from 'zod'
|
|
21
|
+
|
|
22
|
+
export const workspaceListContract = defineContract({
|
|
23
|
+
name: 'workspace.list',
|
|
24
|
+
kind: 'list',
|
|
25
|
+
summary: 'Lista os workspaces do back-office',
|
|
26
|
+
description: 'O workspace é a entidade raiz que agrupa repositórios, apps e agentes…',
|
|
27
|
+
input: z.object({ q: z.string().optional() }),
|
|
28
|
+
output: workspaceRowSchema,
|
|
29
|
+
paginate: 'cursor',
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## A action
|
|
34
|
+
|
|
35
|
+
> `name = <resource>.<verb>`; `kind ∈ simple | form | list | view`; uma por arquivo em
|
|
36
|
+
> `src/domains/<resource>/actions/`, registrada no runtime. Não registrada = não existe.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// api/ — server-only. bindAction amarra o handler no contrato compartilhado.
|
|
40
|
+
import { bindAction } from '@softize/opus/core'
|
|
41
|
+
import { workspaceCreateContract } from '@app/shared' // o mesmo contrato do bloco acima
|
|
42
|
+
|
|
43
|
+
export const workspaceCreate = bindAction(workspaceCreateContract, {
|
|
44
|
+
handler: async (ctx, input) => {
|
|
45
|
+
const id = randomUUID()
|
|
46
|
+
await ctx.db.insertInto('workspaces').values({ id, name: input.name, /* … */ }).execute()
|
|
47
|
+
return { id }
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Ordem canônica dos campos
|
|
53
|
+
|
|
54
|
+
> O `opus check` reprova fora desta ordem — é a régua, 0 violação antes de entregar. A skill
|
|
55
|
+
> `create-action` gera o esqueleto certo por construção.
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
name → kind → (label / summary / messages / tags…)
|
|
59
|
+
→ input → output
|
|
60
|
+
→ (authorize)
|
|
61
|
+
→ fields / paginate
|
|
62
|
+
→ handler
|
|
63
|
+
→ invalidates
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Fail-closed por padrão: `input`/`output` via `z.object` + `t.*` pros tipos lógicos; `authorize`
|
|
67
|
+
declarado ou gate explícito do adapter.
|
|
68
|
+
|
|
69
|
+
Os átomos do catálogo servem TAMBÉM dentro dos schemas de input/output — não re-escreva
|
|
70
|
+
regex do que o Opus já valida: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`.
|
|
71
|
+
O `.zod()` devolve o schema zod subjacente do tipo lógico (slug, email, phone, money…).
|
|
72
|
+
|
|
73
|
+
## Modo design — o mock mora no contrato
|
|
74
|
+
|
|
75
|
+
Um `mockHandler` no contrato deixa a UI rodar com dado realista **sem backend nem banco**: em
|
|
76
|
+
`OPUS_MODE=design` o runtime roteia o `execute` pro `mockHandler` (cai no `handler` real se
|
|
77
|
+
ausente). Como ele fica no CONTRATO (não no `bindAction`), é isomorfo — o design autora, o
|
|
78
|
+
handoff pluga o handler, e o mock nem toca `ctx.db`.
|
|
79
|
+
|
|
80
|
+
Alimente com `fake`/`fakeMany` (fixtures determinísticas do próprio schema — ver [Testes](testing)):
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { fake, fakeMany } from '@softize/opus/testing'
|
|
84
|
+
|
|
85
|
+
export const eventGet = defineContract({
|
|
86
|
+
name: 'event.get',
|
|
87
|
+
kind: 'view',
|
|
88
|
+
input: z.object({ id: z.string().uuid() }),
|
|
89
|
+
output: EventEntity.zod(),
|
|
90
|
+
mockHandler: () => fake(EventEntity.zod()), // 1 evento fake
|
|
91
|
+
})
|
|
92
|
+
// listas: `mockHandler: () => fakeMany(EventEntity.zod(), 20)`
|
|
93
|
+
|
|
94
|
+
// o handoff, depois, só pluga o real — MESMO contrato:
|
|
95
|
+
export const eventGetImpl = bindAction(eventGet, { handler: async (ctx, input) => { /* db */ } })
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
O `mockHandler` roda no **servidor em modo design** (`OPUS_MODE=design`), não no cliente — e
|
|
99
|
+
quem sobe esse servidor é o próprio dev server: o plugin `opusDesign()` (`@softize/opus/vite`)
|
|
100
|
+
monta o runtime Opus DENTRO do vite e serve `/api` in-process, com os mocks respondendo e
|
|
101
|
+
qualquer `server.proxy` pra backend externo desligado (prefixo ex-proxy sem cobertura responde
|
|
102
|
+
503 em envelope — nada vaza pra prod). A SPA chama `/api` normal e recebe o dado fake, isolado:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// vite.config.ts
|
|
106
|
+
import { opusDesign } from '@softize/opus/vite'
|
|
107
|
+
export default defineConfig({ plugins: [react(), opusDesign()] })
|
|
108
|
+
// sobe com: vite --mode design
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
O `entry` (default `./opus.config.ts`) também aceita **glob** — `opusDesign({ entry:
|
|
112
|
+
'src/domains/*/contract.ts' })` — re-expandido a cada rebuild: **domínio novo entra no ar ao
|
|
113
|
+
salvar o arquivo**, sem editar entry nem reiniciar o dev server (criar o arquivo já invalida o
|
|
114
|
+
runtime; o rebuild é lazy, na request seguinte). E o 404 de action desconhecida **se cura
|
|
115
|
+
sozinho**: antes de responder, o plugin força um rebuild (um por ciclo de mudança) e re-tenta —
|
|
116
|
+
persistindo, o envelope traz a mensagem-guia apontando o entry.
|
|
117
|
+
|
|
118
|
+
Como o contrato é isomorfo, o `mockHandler` também acompanha o **bundle web** — peso morto lá
|
|
119
|
+
(a SPA nunca o chama; o dado é fake). Tirá-lo do bundle de prod é um transform de build
|
|
120
|
+
(deferido); **não** dá pra gatear no call-site com `import.meta.env` sem quebrar o servidor
|
|
121
|
+
(não existe em Node). É a materialização Opus-nativa do "mock = contrato": o design não é
|
|
122
|
+
descartável — vira o contrato que o backend honra.
|
|
123
|
+
|
|
124
|
+
## O front consome o contrato
|
|
125
|
+
|
|
126
|
+
> Nunca fetch ou tipos na mão. Os hooks e os patterns (ActionForm/ActionList…) renderizam a
|
|
127
|
+
> action inteira a partir do contrato — o spec é a fonte, o pattern é o renderizador.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { useLookupAction } from '@softize/opus/client'
|
|
131
|
+
import { ActionForm } from '@softize/opus/ui/react'
|
|
132
|
+
|
|
133
|
+
// Lista paginada, tipada pelo contrato — zero shape duplicado.
|
|
134
|
+
const { rows, fetchNextPage } = useLookupAction(workspaceListContract)
|
|
135
|
+
|
|
136
|
+
// Form contract-driven: os campos (fields) moram NO contrato.
|
|
137
|
+
<ActionForm contract={workspaceCreateContract} onSuccess={…} />
|
|
138
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: IA generativa
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# IA generativa
|
|
6
|
+
|
|
7
|
+
> **Experimental.** Superfície mínima — cresce por reincidência de caso real, não por
|
|
8
|
+
> especulação (streaming e memória longa de conversa entram quando um caso real cobrar).
|
|
9
|
+
|
|
10
|
+
Três coisas: **completar** texto, **extrair** dado estruturado, e **rodar um agente** sobre
|
|
11
|
+
as actions do app. O contrato é um adapter do core (`AiAdapter`). O diferencial do `extract`
|
|
12
|
+
é que **o mesmo `Schema` das actions vira o contrato da resposta do modelo** — saída forçada
|
|
13
|
+
e validada pelo schema. O do `run` é que **as actions viram as tools** — o modelo age no app,
|
|
14
|
+
como o usuário, pelas mesmas actions que a UI usa.
|
|
15
|
+
|
|
16
|
+
## O contrato
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface AiAdapter {
|
|
20
|
+
complete(prompt: string, opts?: AiCompleteOptions): Promise<string>
|
|
21
|
+
extract<T>(prompt: string, schema: Schema<T>, opts?: AiCompleteOptions): Promise<T>
|
|
22
|
+
// Loop agêntico: o modelo chama tools (as actions ai:enabled) até responder em texto.
|
|
23
|
+
run(input: string | AiMessage[], opts): Promise<AiRunResult>
|
|
24
|
+
}
|
|
25
|
+
// opts: { system?, model?, maxTokens?, temperature? } — tudo tem default do driver.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Driver
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { anthropicAi } from '@softize/opus/ai/anthropic'
|
|
32
|
+
|
|
33
|
+
// Default: haiku (rápido/barato) e ANTHROPIC_API_KEY do ambiente.
|
|
34
|
+
const ai = anthropicAi()
|
|
35
|
+
|
|
36
|
+
// Tudo injetável: client próprio, modelo default, teto de tokens.
|
|
37
|
+
const custom = anthropicAi({ apiKey, model: 'claude-sonnet-5', maxTokens: 2048 })
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
O peer é **opcional** (`@anthropic-ai/sdk`): quem não usa o driver não o instala —
|
|
41
|
+
importado sob demanda. Em teste, injete um client fake (`{ messages: { create } }`)
|
|
42
|
+
e nada toca a rede.
|
|
43
|
+
|
|
44
|
+
## No runtime
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const runtime = createRuntime({
|
|
48
|
+
// …server/data/auth…
|
|
49
|
+
ai: anthropicAi(),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// No handler: ctx.ai (null quando não configurado).
|
|
53
|
+
handler: async (ctx, input) => {
|
|
54
|
+
const meta = await ctx.ai!.extract(
|
|
55
|
+
`Classifique o chamado e proponha um título curto: ${input.texto}`,
|
|
56
|
+
z.object({ titulo: z.string(), prioridade: z.enum(['baixa', 'media', 'alta']) }),
|
|
57
|
+
)
|
|
58
|
+
return meta // já validado pelo schema — fora do contrato, o extract estoura.
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Actions como tools (o agente)
|
|
63
|
+
|
|
64
|
+
Marque uma action com `ai: { enabled: true }` no contrato e ela vira uma **tool** que o
|
|
65
|
+
modelo pode chamar. Aí `ctx.ai.run(prompt)` roda o loop agêntico sobre as actions `ai:enabled`:
|
|
66
|
+
o modelo escolhe a tool → o runtime executa a action **como o usuário logado** (limitado pelo
|
|
67
|
+
`ctx.can`) → o resultado volta pro modelo → repete até a resposta em texto.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// A action opta por entrar — o contrato já tem o schema, que vira a tool spec de graça:
|
|
71
|
+
export const buscarNotas = defineContract({
|
|
72
|
+
name: 'nota.buscar',
|
|
73
|
+
kind: 'list',
|
|
74
|
+
input: z.object({ cliente: z.string(), mes: z.string() }),
|
|
75
|
+
output: z.object({ total: z.number() }),
|
|
76
|
+
ai: { enabled: true, description: 'Busca notas por cliente e mês.' },
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// No handler — ou fora dele, via runtime.aiFor(base), pro backend de um chat:
|
|
80
|
+
const { text } = await ctx.ai!.run('Quantas notas a Empresa X emitiu em junho?')
|
|
81
|
+
// o modelo chamou nota.buscar sozinho, como o usuário logado, e respondeu em texto.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
O que é `destructive` ou pede `requiresConfirmation` **não roda sem aprovação**: passe
|
|
85
|
+
`run(prompt, { confirm })` — o chat mostra o "confirmar?"; sem isso, a action é recusada e o
|
|
86
|
+
modelo avisa. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto —
|
|
87
|
+
o backend do chat resolve o usuário e chama `.run(historico)`.
|
|
88
|
+
|
|
89
|
+
## Streaming — o protocolo de eventos de conversa
|
|
90
|
+
|
|
91
|
+
`runStream` é o mesmo loop agêntico do `run`, emitindo **`ChatEvent`** conforme acontece
|
|
92
|
+
(o contrato: `docs/chat-event-protocol.md`): `text` (delta incremental), `tool` (a action
|
|
93
|
+
em uso — dado cru; humanizar é da apresentação), `artifact` (algo produzido na conversa)
|
|
94
|
+
e `done`. Aditivo: o `run` clássico permanece, e `Promise<string>` segue sendo o caso
|
|
95
|
+
degenerado do protocolo (um `text` + um `done`).
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// O backend de um chat streamando por SSE:
|
|
99
|
+
for await (const event of runtime.aiFor(base)!.runStream(messages)) {
|
|
100
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
O driver Anthropic emite os deltas token a token quando o client suporta `messages.stream`;
|
|
105
|
+
com um client só-`create` (ex.: fake de teste), o texto de cada passo sai como delta único.
|
|
106
|
+
O `<Chat>` do opus/ui consome os dois modos — ver a página **Chat**.
|
|
107
|
+
|
|
108
|
+
## Limites (por enquanto)
|
|
109
|
+
|
|
110
|
+
O `run`/`runStream` fiam histórico multi-turn, mas sem memória longa/resumo automático.
|
|
111
|
+
`extract` exige schema com objeto na raiz (`z.object`) — regra da API de tools do provider
|
|
112
|
+
e o formato natural de um contrato.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
## Quase sempre é o `dialog`
|
|
2
|
+
|
|
3
|
+
Pergunta de sim/não, um aviso a reconhecer, uma string — **não monte isto à mão**: o trio
|
|
4
|
+
`dialog.confirm/alert/prompt` faz em uma linha, e o `body` cobre até corpo com conteúdo
|
|
5
|
+
próprio (lista, detalhe). A demo viva mora na [página do dialog](/ui/confirm). Este
|
|
6
|
+
componente é o **primitivo por baixo deles**.
|
|
7
|
+
|
|
8
|
+
Então **componha o AlertDialog à mão só quando o trio não alcança**: mais de duas ações, ou
|
|
9
|
+
um layout totalmente custom. É o que as seções abaixo mostram.
|
|
10
|
+
|
|
11
|
+
## Mais de duas ações
|
|
12
|
+
|
|
13
|
+
O caso que o `confirm` (binário) não expressa: três saídas. `AlertDialogTrigger` (asChild
|
|
14
|
+
com Button) abre; cada `AlertDialogAction`/`AlertDialogCancel` é uma saída. Diferente do
|
|
15
|
+
Dialog, **não fecha clicando fora** — exige uma escolha.
|
|
16
|
+
|
|
17
|
+
```tsx preview
|
|
18
|
+
<AlertDialog>
|
|
19
|
+
<AlertDialogTrigger asChild>
|
|
20
|
+
<Button variant="outline">Fechar editor</Button>
|
|
21
|
+
</AlertDialogTrigger>
|
|
22
|
+
<AlertDialogContent>
|
|
23
|
+
<AlertDialogHeader>
|
|
24
|
+
<AlertDialogTitle>Alterações não salvas</AlertDialogTitle>
|
|
25
|
+
<AlertDialogDescription>
|
|
26
|
+
Você editou o contrato e ainda não salvou. O que fazer antes de fechar?
|
|
27
|
+
</AlertDialogDescription>
|
|
28
|
+
</AlertDialogHeader>
|
|
29
|
+
<AlertDialogFooter>
|
|
30
|
+
<AlertDialogCancel>Continuar editando</AlertDialogCancel>
|
|
31
|
+
<AlertDialogAction variant="outline">Descartar</AlertDialogAction>
|
|
32
|
+
<AlertDialogAction>Salvar e fechar</AlertDialogAction>
|
|
33
|
+
</AlertDialogFooter>
|
|
34
|
+
</AlertDialogContent>
|
|
35
|
+
</AlertDialog>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## A estrutura e o botão destrutivo
|
|
39
|
+
|
|
40
|
+
`AlertDialogHeader` (media/título/descrição) + `AlertDialogFooter` (as ações).
|
|
41
|
+
`variant="destructive"` no `AlertDialogAction` (ele herda variant/size do Button) pinta o
|
|
42
|
+
confirmar de vermelho. Uma confirmação destrutiva **binária** é só
|
|
43
|
+
`dialog.confirm({ variant: 'destructive' })` — o exemplo abaixo é só pra mostrar a
|
|
44
|
+
composição e onde o `variant` entra:
|
|
45
|
+
|
|
46
|
+
```tsx preview
|
|
47
|
+
<AlertDialog>
|
|
48
|
+
<AlertDialogTrigger asChild>
|
|
49
|
+
<Button variant="destructive">Excluir repositório</Button>
|
|
50
|
+
</AlertDialogTrigger>
|
|
51
|
+
<AlertDialogContent>
|
|
52
|
+
<AlertDialogHeader>
|
|
53
|
+
<AlertDialogTitle>Excluir empresa-x-api?</AlertDialogTitle>
|
|
54
|
+
<AlertDialogDescription>
|
|
55
|
+
O repositório sai do workspace e os agentes vinculados perdem o acesso ao código. Não dá pra desfazer.
|
|
56
|
+
</AlertDialogDescription>
|
|
57
|
+
</AlertDialogHeader>
|
|
58
|
+
<AlertDialogFooter>
|
|
59
|
+
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
|
60
|
+
<AlertDialogAction variant="destructive">Excluir</AlertDialogAction>
|
|
61
|
+
</AlertDialogFooter>
|
|
62
|
+
</AlertDialogContent>
|
|
63
|
+
</AlertDialog>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Props
|
|
67
|
+
|
|
68
|
+
| Prop | Tipo | Default | Descrição |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| `open` (AlertDialog) | `boolean` | | Estado de aberto no modo controlado — pareie com onOpenChange. No padrão, o Trigger cuida disso. |
|
|
71
|
+
| `onOpenChange` (AlertDialog) | `(open: boolean) => void` | | Chamado quando o diálogo abre ou fecha (Trigger, Cancel, Action ou Esc). |
|
|
72
|
+
| `variant` (AlertDialogAction) | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | Intenção do botão de confirmar (herdada do Button) — destructive pra ação perigosa. |
|
|
73
|
+
| `variant` (AlertDialogCancel) | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'outline'` | Intenção do botão de cancelar (herdada do Button). |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
## Forma curta
|
|
2
|
+
|
|
3
|
+
Quase todo alert é ícone + título + uma frase — então isso é UMA linha: `title`, `description` e `icon` como props. O componente monta os slots e a a11y (`role="alert"`, que faz o leitor de tela anunciar sozinho).
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
<Alert icon={<Info />} title="Opus 2.8.0" description="Este workspace usa a versão pinada em opus.json." />
|
|
7
|
+
<Alert
|
|
8
|
+
variant="destructive"
|
|
9
|
+
icon={<CircleAlert />}
|
|
10
|
+
title="Sessão encerrada"
|
|
11
|
+
description="O agente parou antes de concluir. Veja o detalhe no log da sessão."
|
|
12
|
+
/>
|
|
13
|
+
<Alert
|
|
14
|
+
variant="success"
|
|
15
|
+
icon={<CircleCheck />}
|
|
16
|
+
title="Skill publicada"
|
|
17
|
+
description="Os agentes do workspace já enxergam a nova versão."
|
|
18
|
+
/>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Só a frase
|
|
22
|
+
|
|
23
|
+
Título é opcional — o aviso de uma linha dispensa. `destructive` e `success` mantêm superfície tonalizada + borda (divergência da casa: o shadcn rebaixou destructive pra `bg-card`, nós preservamos o realce).
|
|
24
|
+
|
|
25
|
+
```tsx preview col
|
|
26
|
+
<Alert description="Nenhuma sessão aberta neste repositório." />
|
|
27
|
+
<Alert variant="success" icon={<CircleCheck />} description="Workspace Empresa X sincronizado." />
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Ícone é opcional
|
|
31
|
+
|
|
32
|
+
Sem `icon` o alert é bloco comum; com ele, vira grid de duas colunas e o conteúdo se alinha ao lado. Texto solto como filho também vale (`<Alert>Sincronizado.</Alert>`) — cai no slot de descrição sozinho.
|
|
33
|
+
|
|
34
|
+
```tsx preview col
|
|
35
|
+
<Alert title="Sem provider próprio" description="As conversas usam o padrão do sistema." />
|
|
36
|
+
<Alert variant="success">Workspace Empresa X sincronizado.</Alert>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Composição (conteúdo rico)
|
|
40
|
+
|
|
41
|
+
Quando a descrição tem mais que uma frase — parágrafos, lista, um botão — componha: `AlertTitle` e `AlertDescription` seguem exportados, e o espaço entre BLOCOS filhos vem de graça.
|
|
42
|
+
|
|
43
|
+
```tsx preview col
|
|
44
|
+
<Alert variant="destructive" icon={<CircleAlert />}>
|
|
45
|
+
<AlertTitle>Não deu pra publicar</AlertTitle>
|
|
46
|
+
<AlertDescription>
|
|
47
|
+
<p>O registry recusou a versão 3.0.0 — ela já existe.</p>
|
|
48
|
+
<p>Suba o patch e tente de novo.</p>
|
|
49
|
+
</AlertDescription>
|
|
50
|
+
</Alert>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Os dois modos convivem: com `title`/`description` preenchidos, `children` entra DEPOIS da frase — é onde vai a ação.
|
|
54
|
+
|
|
55
|
+
```tsx preview col
|
|
56
|
+
<Alert icon={<CircleAlert />} title="Sessão presa" description="O ambiente não subiu no tempo esperado.">
|
|
57
|
+
<Button size="sm" variant="outline">Reiniciar</Button>
|
|
58
|
+
</Alert>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Props
|
|
62
|
+
|
|
63
|
+
| Prop | Tipo | Default | Descrição |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `title` | `React.ReactNode` | | Título do alert (a forma curta). Não é o atributo `title` do HTML — esse é tooltip nativo, banido na casa, e o componente não o aceita. |
|
|
66
|
+
| `description` | `React.ReactNode` | | A frase. Sozinha, dispensa título. |
|
|
67
|
+
| `icon` | `React.ReactNode` | | Ícone à esquerda; é ele que liga o layout de duas colunas. Decorativo — quem nomeia é o título. |
|
|
68
|
+
| `variant` | `'default' \| 'destructive' \| 'success'` | `'default'` | O tom da mensagem. |
|
|
69
|
+
| `children` | `React.ReactNode` | | Composição (`AlertTitle`/`AlertDescription`), texto cru, ou — junto da forma curta — o que vem depois da frase. |
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
## Chrome da aplicação
|
|
2
|
+
|
|
3
|
+
O esqueleto que todo app da casa repete: sidebar de navegação à esquerda (header, nav rolável, rodapé ancorado) e o conteúdo. É o quadro — o que vai em cada slot (a marca, o seletor de contexto, o nav do app) é do consumidor. Pro esqueleto de *uma página* (título, descrição, ação), use o `Page` dentro do conteúdo.
|
|
4
|
+
|
|
5
|
+
```tsx preview col
|
|
6
|
+
render(
|
|
7
|
+
<div className="w-full overflow-hidden rounded-lg border border-border">
|
|
8
|
+
<AppShell
|
|
9
|
+
className="h-96"
|
|
10
|
+
sidebarHeader={<div className="px-2.5 py-2 text-sm font-semibold">◆ Acme</div>}
|
|
11
|
+
sidebarNav={
|
|
12
|
+
<nav className="space-y-1 pt-2">
|
|
13
|
+
{['Clientes', 'Workspaces', 'Agentes'].map((item, index) => (
|
|
14
|
+
<button
|
|
15
|
+
key={item}
|
|
16
|
+
type="button"
|
|
17
|
+
className={
|
|
18
|
+
'flex w-full items-center rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-muted/60 ' +
|
|
19
|
+
(index === 0 ? 'bg-muted' : 'text-foreground/80')
|
|
20
|
+
}
|
|
21
|
+
>
|
|
22
|
+
{item}
|
|
23
|
+
</button>
|
|
24
|
+
))}
|
|
25
|
+
</nav>
|
|
26
|
+
}
|
|
27
|
+
sidebarFooter={<div className="px-2.5 py-2 text-xs text-muted-foreground">ana@acme.com</div>}
|
|
28
|
+
>
|
|
29
|
+
<div className="m-4 flex-1 rounded-lg border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
|
|
30
|
+
O conteúdo (em geral um Page).
|
|
31
|
+
</div>
|
|
32
|
+
</AppShell>
|
|
33
|
+
</div>,
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Recolher a sidebar
|
|
38
|
+
|
|
39
|
+
`collapsible` é **opt-in** — sem ele o shell é o de sempre. O shell controla a **largura** e publica o estado; **o que some é decisão do conteúdo**, porque o `sidebarNav` é seu. Em vez de te obrigar a gerenciar estado, o aside expõe `data-collapsed` e o grupo `sidebar`: o rótulo some por CSS.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
<span className="group-data-[collapsed=true]/sidebar:hidden">Clientes</span>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
O botão é o `<AppShellTrigger />`, que **você** posiciona (numa `AppShellBar`, no header do conteúdo…) — ele some sozinho quando o shell não é `collapsible`, então não precisa de condicional. Pra ler o estado em JS, `useAppShell()`. Pra persistir a preferência, controle de fora com `collapsed` + `onCollapsedChange`; `sidebarCollapsedClassName` ajusta a largura recolhida (default `w-14`).
|
|
46
|
+
|
|
47
|
+
Se você passa a própria largura em `sidebarClassName`, ela vale no **expandido**: no recolhido quem vence é a largura do recolhido — recolher é estado, não estilo default, e o contrário deixaria o aside meio-recolhido (rótulo some, largura fica).
|
|
48
|
+
|
|
49
|
+
```tsx preview col
|
|
50
|
+
render(
|
|
51
|
+
<div className="w-full overflow-hidden rounded-lg border border-border">
|
|
52
|
+
<AppShell
|
|
53
|
+
collapsible
|
|
54
|
+
className="h-96"
|
|
55
|
+
sidebarHeader={
|
|
56
|
+
<div className="flex h-12 items-center gap-2 border-b border-border px-2.5">
|
|
57
|
+
<AppShellTrigger />
|
|
58
|
+
<span className="text-sm font-semibold group-data-[collapsed=true]/sidebar:hidden">◆ Acme</span>
|
|
59
|
+
</div>
|
|
60
|
+
}
|
|
61
|
+
sidebarNav={
|
|
62
|
+
<nav className="space-y-1 pt-2">
|
|
63
|
+
{[
|
|
64
|
+
{ label: 'Clientes', icon: '👤' },
|
|
65
|
+
{ label: 'Workspaces', icon: '▦' },
|
|
66
|
+
{ label: 'Agentes', icon: '✦' },
|
|
67
|
+
].map((item, index) => (
|
|
68
|
+
<button
|
|
69
|
+
key={item.label}
|
|
70
|
+
type="button"
|
|
71
|
+
title={item.label}
|
|
72
|
+
className={
|
|
73
|
+
'flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors hover:bg-muted/60 ' +
|
|
74
|
+
(index === 0 ? 'bg-muted' : 'text-foreground/80')
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
<span className="shrink-0">{item.icon}</span>
|
|
78
|
+
<span className="truncate group-data-[collapsed=true]/sidebar:hidden">{item.label}</span>
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
</nav>
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
<div className="m-4 flex-1 rounded-lg border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
|
|
85
|
+
Clique no botão da sidebar pra recolher.
|
|
86
|
+
</div>
|
|
87
|
+
</AppShell>
|
|
88
|
+
</div>,
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Com rail à direita
|
|
93
|
+
|
|
94
|
+
`rail` liga o terceiro painel — o lugar do chat de agente ou de um inspetor. Conteúdo e rail viram painéis redimensionáveis (arraste a divisória); `mainDefaultSize`/`mainMinSize`/`railMinSize` (em %) calibram a partilha.
|
|
95
|
+
|
|
96
|
+
```tsx preview col
|
|
97
|
+
render(
|
|
98
|
+
<div className="w-full overflow-hidden rounded-lg border border-border">
|
|
99
|
+
<AppShell
|
|
100
|
+
className="h-96"
|
|
101
|
+
sidebarHeader={<div className="px-2.5 py-2 text-sm font-semibold">◆ Acme</div>}
|
|
102
|
+
sidebarNav={
|
|
103
|
+
<nav className="space-y-1 pt-2">
|
|
104
|
+
<button type="button" className="flex w-full items-center rounded-md bg-muted px-2.5 py-1.5 text-left text-sm">
|
|
105
|
+
Painel comercial
|
|
106
|
+
</button>
|
|
107
|
+
</nav>
|
|
108
|
+
}
|
|
109
|
+
rail={
|
|
110
|
+
<div className="flex h-full flex-col border-l bg-background">
|
|
111
|
+
<div className="border-b px-3 py-2 text-xs text-muted-foreground">Conversa</div>
|
|
112
|
+
<div className="flex-1 p-3 text-sm text-muted-foreground">O chat do agente vive aqui.</div>
|
|
113
|
+
</div>
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<div className="m-4 flex-1 rounded-lg border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
|
|
117
|
+
O conteúdo (preview, tabela, página).
|
|
118
|
+
</div>
|
|
119
|
+
</AppShell>
|
|
120
|
+
</div>,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
O shell não impõe fundo nem borda aos slots — ele é TRANSPARENTE: o canvas (cinza clarinho) vem do `body` (theme.css) e a sidebar o herda; um rail tipicamente traz `border-l bg-background`, como acima.
|
|
125
|
+
|
|
126
|
+
## Chrome flush (linha de header contínua)
|
|
127
|
+
|
|
128
|
+
`flush` tira o inset: a sidebar perde o padding do shell e o conteúdo vira full-bleed
|
|
129
|
+
apartado por um filete à esquerda — o shell NÃO pinta fundo (o canvas do body aparece);
|
|
130
|
+
superfície branca é decisão do conteúdo (`bg-background` no seu contêiner). Combine com `AppShellBar` — a faixa h-12 com `border-b` —
|
|
131
|
+
uma na sidebar (a marca) e outra no topo do conteúdo (breadcrumb/ações): as alturas
|
|
132
|
+
casam e a linha do header atravessa a tela inteira.
|
|
133
|
+
|
|
134
|
+
```tsx preview col lg
|
|
135
|
+
render(
|
|
136
|
+
<AppShell
|
|
137
|
+
className="h-96 rounded-lg border"
|
|
138
|
+
flush
|
|
139
|
+
sidebarHeader={
|
|
140
|
+
<AppShellBar>
|
|
141
|
+
<span className="grid h-6 w-6 place-items-center rounded-md bg-primary/10 text-sm text-primary">◆</span>
|
|
142
|
+
<span className="text-sm font-semibold tracking-tight">Grupo Vetra</span>
|
|
143
|
+
</AppShellBar>
|
|
144
|
+
}
|
|
145
|
+
sidebarNav={<nav className="p-2 text-sm text-muted-foreground">Relatórios…</nav>}
|
|
146
|
+
>
|
|
147
|
+
<AppShellBar>
|
|
148
|
+
<span className="text-muted-foreground">Vetra BI</span>
|
|
149
|
+
<span className="text-muted-foreground/40">›</span>
|
|
150
|
+
<span className="text-sm font-medium">Painel comercial</span>
|
|
151
|
+
</AppShellBar>
|
|
152
|
+
<div className="p-4 text-sm text-muted-foreground">Conteúdo full-bleed.</div>
|
|
153
|
+
</AppShell>,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
## Padrão (16/9)
|
|
2
|
+
|
|
3
|
+
O `ratio` é a proporção entre largura e altura — 16/9 é o padrão de vídeo. Defina a largura no
|
|
4
|
+
contêiner (o pai): a altura o componente deriva sozinho.
|
|
5
|
+
|
|
6
|
+
```tsx preview col
|
|
7
|
+
<div className="w-full max-w-md">
|
|
8
|
+
<AspectRatio ratio={16 / 9} className="overflow-hidden rounded-lg border bg-muted">
|
|
9
|
+
<div className="flex h-full w-full items-center justify-center text-sm text-muted-foreground">
|
|
10
|
+
Preview do worktree — Empresa X
|
|
11
|
+
</div>
|
|
12
|
+
</AspectRatio>
|
|
13
|
+
</div>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quadrado (1/1)
|
|
17
|
+
|
|
18
|
+
ratio={1} trava num quadrado — o formato dos avatares de agente e dos ícones de skill, onde a
|
|
19
|
+
moldura precisa ser previsível.
|
|
20
|
+
|
|
21
|
+
```tsx preview col-start
|
|
22
|
+
<div className="w-40">
|
|
23
|
+
<AspectRatio ratio={1} className="overflow-hidden rounded-lg border bg-muted">
|
|
24
|
+
<div className="flex h-full w-full items-center justify-center text-sm font-medium text-muted-foreground">
|
|
25
|
+
developer
|
|
26
|
+
</div>
|
|
27
|
+
</AspectRatio>
|
|
28
|
+
</div>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Galeria de previews
|
|
32
|
+
|
|
33
|
+
Mesmo ratio em vários cards: as molduras alinham antes mesmo do conteúdo chegar — sem o pulo de
|
|
34
|
+
layout quando o preview carrega.
|
|
35
|
+
|
|
36
|
+
```tsx preview col
|
|
37
|
+
<div className="grid w-full grid-cols-3 gap-3">
|
|
38
|
+
{[
|
|
39
|
+
{ ref: 'empresa-x-api', branch: 'feat/preview-pipeline' },
|
|
40
|
+
{ ref: 'empresa-x-web', branch: 'fix/handoff-modal' },
|
|
41
|
+
{ ref: 'empresa-x-infra', branch: 'chore/caddy-bump' },
|
|
42
|
+
].map((session) => (
|
|
43
|
+
<AspectRatio
|
|
44
|
+
key={session.ref}
|
|
45
|
+
ratio={16 / 9}
|
|
46
|
+
className="overflow-hidden rounded-lg border bg-muted"
|
|
47
|
+
>
|
|
48
|
+
<div className="flex h-full w-full flex-col justify-end gap-1 p-2">
|
|
49
|
+
<span className="text-sm font-medium">{session.ref}</span>
|
|
50
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
51
|
+
<GitBranch className="size-3" />
|
|
52
|
+
{session.branch}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
</AspectRatio>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
|
|
62
|
+
| Prop | Tipo | Default | Descrição |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `ratio` | `number` | `1` | A razão largura/altura. 16/9 pra vídeo/preview, 1 pra quadrado, 4/3 pra clássico. |
|
|
65
|
+
| `children` | `React.ReactNode` | | O conteúdo a enquadrar (img, iframe, div) — preencha com h-full w-full e object-cover. |
|
|
66
|
+
| `className` | `string` | | Estilo do bloco — borda, rounded e overflow-hidden moram aqui, não no filho. |
|