@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.
Files changed (286) hide show
  1. package/CHANGELOG.md +1616 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/bin/cli.mjs +528 -0
  5. package/bin/lib/check.mjs +307 -0
  6. package/bin/lib/components.mjs +151 -0
  7. package/bin/lib/create.mjs +208 -0
  8. package/bin/lib/db-check-runner.mjs +86 -0
  9. package/bin/lib/db-migrate-runner.mjs +89 -0
  10. package/bin/lib/db-scaffold-runner.mjs +84 -0
  11. package/bin/lib/db.mjs +261 -0
  12. package/bin/lib/docs-include.mjs +48 -0
  13. package/bin/lib/gen-dicts.mjs +134 -0
  14. package/bin/lib/gen-docs.mjs +288 -0
  15. package/bin/lib/gen-manifest.mjs +102 -0
  16. package/bin/lib/gen-openapi.mjs +195 -0
  17. package/bin/lib/gen-runner.mjs +472 -0
  18. package/bin/lib/gen-stubs.mjs +463 -0
  19. package/bin/lib/gen.mjs +311 -0
  20. package/bin/lib/init.mjs +514 -0
  21. package/bin/lib/introspect.mjs +107 -0
  22. package/bin/lib/mcp.mjs +85 -0
  23. package/bin/lib/postinstall.mjs +56 -0
  24. package/docs/chat-event-protocol.md +85 -0
  25. package/docs/code-style.md +16 -0
  26. package/docs/data-layer.md +246 -0
  27. package/docs/ownership-vs-shadcn-lock.md +102 -0
  28. package/docs/protocol.md +2053 -0
  29. package/docs/releasing.md +110 -0
  30. package/docs/shellnav.md +131 -0
  31. package/package.json +338 -0
  32. package/registry/hooks/hooks.json +26 -0
  33. package/registry/hooks/link-memory-on-start.mjs +46 -0
  34. package/registry/hooks/opus-check-on-stop.mjs +114 -0
  35. package/registry/skills/create-action/SKILL.md +49 -0
  36. package/registry/skills/create-action/scaffold.mjs +122 -0
  37. package/registry/templates/app/_gitignore +3 -0
  38. package/registry/templates/app/_npmrc +1 -0
  39. package/registry/templates/app/_opus/_gitignore +5 -0
  40. package/registry/templates/app/_prettierrc.json +6 -0
  41. package/registry/templates/app/index.html +13 -0
  42. package/registry/templates/app/opus.config.ts +16 -0
  43. package/registry/templates/app/package.json +43 -0
  44. package/registry/templates/app/pnpm-workspace.yaml +11 -0
  45. package/registry/templates/app/public/favicon.svg +4 -0
  46. package/registry/templates/app/src/App.tsx +37 -0
  47. package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
  48. package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
  49. package/registry/templates/app/src/domains/tasks/index.ts +13 -0
  50. package/registry/templates/app/src/index.css +18 -0
  51. package/registry/templates/app/src/main.tsx +25 -0
  52. package/registry/templates/app/tsconfig.json +20 -0
  53. package/registry/templates/app/vite.config.ts +46 -0
  54. package/registry/templates/monorepo/_gitignore +3 -0
  55. package/registry/templates/monorepo/_npmrc +1 -0
  56. package/registry/templates/monorepo/package.json +9 -0
  57. package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
  58. package/src/ai/ask.ts +64 -0
  59. package/src/ai/drivers/anthropic.ts +309 -0
  60. package/src/ai/index.ts +17 -0
  61. package/src/audit/drivers/console.ts +117 -0
  62. package/src/audit/drivers/pg.ts +172 -0
  63. package/src/audit/index.ts +51 -0
  64. package/src/auth/drivers/better-auth.ts +103 -0
  65. package/src/auth/drivers/jwt.ts +188 -0
  66. package/src/auth/index.ts +9 -0
  67. package/src/client/drivers/fetch.ts +202 -0
  68. package/src/client/index.ts +22 -0
  69. package/src/core/actions.ts +110 -0
  70. package/src/core/audit.ts +239 -0
  71. package/src/core/contracts.ts +137 -0
  72. package/src/core/domain.ts +310 -0
  73. package/src/core/errors.ts +181 -0
  74. package/src/core/index.ts +174 -0
  75. package/src/core/logical-type.ts +31 -0
  76. package/src/core/reactions.ts +81 -0
  77. package/src/core/runtime.ts +1167 -0
  78. package/src/core/schedules.ts +41 -0
  79. package/src/core/types.ts +1356 -0
  80. package/src/data/drivers/kysely.ts +389 -0
  81. package/src/data/index.ts +10 -0
  82. package/src/data/readonly-pool.ts +160 -0
  83. package/src/dsl/eval.ts +136 -0
  84. package/src/dsl/index.ts +29 -0
  85. package/src/dsl/kysely.ts +230 -0
  86. package/src/dsl/loads.ts +123 -0
  87. package/src/dsl/parser.ts +423 -0
  88. package/src/dsl/types.ts +113 -0
  89. package/src/events/drivers/mitt.ts +70 -0
  90. package/src/events/index.ts +9 -0
  91. package/src/log/drivers/pino.ts +57 -0
  92. package/src/log/index.ts +9 -0
  93. package/src/mcp/index.ts +62 -0
  94. package/src/queue/drivers/bullmq.ts +190 -0
  95. package/src/queue/index.ts +9 -0
  96. package/src/scheduler/drivers/node-cron.ts +93 -0
  97. package/src/scheduler/every.ts +45 -0
  98. package/src/scheduler/index.ts +9 -0
  99. package/src/schema/drivers/zod.ts +765 -0
  100. package/src/schema/entity.ts +439 -0
  101. package/src/schema/format/locale.ts +144 -0
  102. package/src/schema/index.ts +65 -0
  103. package/src/schema/openapi.ts +302 -0
  104. package/src/schema/scaffold.ts +160 -0
  105. package/src/server/drivers/fastify.ts +224 -0
  106. package/src/server/drivers/node.ts +386 -0
  107. package/src/server/index.ts +142 -0
  108. package/src/storage/drivers/fs.ts +90 -0
  109. package/src/storage/drivers/s3.ts +117 -0
  110. package/src/storage/index.ts +27 -0
  111. package/src/testing/fake.ts +298 -0
  112. package/src/testing/index.ts +324 -0
  113. package/src/ui/components/patterns/action-form-card.tsx +48 -0
  114. package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
  115. package/src/ui/components/patterns/app-shell.tsx +227 -0
  116. package/src/ui/components/patterns/confirm.tsx +226 -0
  117. package/src/ui/components/patterns/data-state.tsx +75 -0
  118. package/src/ui/components/patterns/form-dialog.tsx +64 -0
  119. package/src/ui/components/patterns/form.tsx +584 -0
  120. package/src/ui/components/patterns/list.tsx +1488 -0
  121. package/src/ui/components/patterns/page.tsx +46 -0
  122. package/src/ui/components/patterns/section-shell.tsx +246 -0
  123. package/src/ui/components/patterns/shell-nav.tsx +150 -0
  124. package/src/ui/components/patterns/sidebar.tsx +89 -0
  125. package/src/ui/components/patterns/split.tsx +93 -0
  126. package/src/ui/components/patterns/trigger.tsx +196 -0
  127. package/src/ui/components/patterns/view.tsx +84 -0
  128. package/src/ui/components/primitives/accordion.tsx +64 -0
  129. package/src/ui/components/primitives/alert-dialog.tsx +190 -0
  130. package/src/ui/components/primitives/alert.tsx +116 -0
  131. package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
  132. package/src/ui/components/primitives/avatar.tsx +107 -0
  133. package/src/ui/components/primitives/badge.tsx +37 -0
  134. package/src/ui/components/primitives/breadcrumb.tsx +109 -0
  135. package/src/ui/components/primitives/button-group.tsx +83 -0
  136. package/src/ui/components/primitives/button.tsx +102 -0
  137. package/src/ui/components/primitives/calendar.tsx +218 -0
  138. package/src/ui/components/primitives/card.tsx +56 -0
  139. package/src/ui/components/primitives/carousel.tsx +239 -0
  140. package/src/ui/components/primitives/chat.tsx +407 -0
  141. package/src/ui/components/primitives/checkbox.tsx +30 -0
  142. package/src/ui/components/primitives/collapsible.tsx +31 -0
  143. package/src/ui/components/primitives/command.tsx +182 -0
  144. package/src/ui/components/primitives/composer.tsx +121 -0
  145. package/src/ui/components/primitives/copyable.tsx +50 -0
  146. package/src/ui/components/primitives/dialog.tsx +147 -0
  147. package/src/ui/components/primitives/drawer.tsx +141 -0
  148. package/src/ui/components/primitives/empty.tsx +104 -0
  149. package/src/ui/components/primitives/field.tsx +246 -0
  150. package/src/ui/components/primitives/icon-picker.tsx +180 -0
  151. package/src/ui/components/primitives/input-group.tsx +168 -0
  152. package/src/ui/components/primitives/input-otp.tsx +75 -0
  153. package/src/ui/components/primitives/input.tsx +72 -0
  154. package/src/ui/components/primitives/item.tsx +193 -0
  155. package/src/ui/components/primitives/kbd.tsx +28 -0
  156. package/src/ui/components/primitives/label.tsx +22 -0
  157. package/src/ui/components/primitives/markdown.tsx +35 -0
  158. package/src/ui/components/primitives/menu.tsx +255 -0
  159. package/src/ui/components/primitives/pagination.tsx +127 -0
  160. package/src/ui/components/primitives/popover.tsx +87 -0
  161. package/src/ui/components/primitives/progress.tsx +29 -0
  162. package/src/ui/components/primitives/radio-group.tsx +43 -0
  163. package/src/ui/components/primitives/resizable.tsx +51 -0
  164. package/src/ui/components/primitives/scroll-area.tsx +56 -0
  165. package/src/ui/components/primitives/select.tsx +479 -0
  166. package/src/ui/components/primitives/separator.tsx +26 -0
  167. package/src/ui/components/primitives/skeleton.tsx +13 -0
  168. package/src/ui/components/primitives/slider.tsx +61 -0
  169. package/src/ui/components/primitives/sonner.tsx +46 -0
  170. package/src/ui/components/primitives/spinner.tsx +29 -0
  171. package/src/ui/components/primitives/switch.tsx +33 -0
  172. package/src/ui/components/primitives/table.tsx +114 -0
  173. package/src/ui/components/primitives/tabs.tsx +104 -0
  174. package/src/ui/components/primitives/textarea.tsx +18 -0
  175. package/src/ui/components/primitives/toggle-group.tsx +81 -0
  176. package/src/ui/components/primitives/toggle.tsx +45 -0
  177. package/src/ui/components/primitives/tooltip.tsx +55 -0
  178. package/src/ui/components/primitives/truncate.tsx +49 -0
  179. package/src/ui/docs/DocBrowser.tsx +90 -0
  180. package/src/ui/docs/changelog.tsx +80 -0
  181. package/src/ui/docs/content/accordion.md +86 -0
  182. package/src/ui/docs/content/action-form-card.md +24 -0
  183. package/src/ui/docs/content/action-form-dialog.md +30 -0
  184. package/src/ui/docs/content/action-form.md +125 -0
  185. package/src/ui/docs/content/action-list-dialog.md +68 -0
  186. package/src/ui/docs/content/action-list.md +194 -0
  187. package/src/ui/docs/content/action-trigger.md +72 -0
  188. package/src/ui/docs/content/action-view.md +47 -0
  189. package/src/ui/docs/content/actions.md +138 -0
  190. package/src/ui/docs/content/ai.md +112 -0
  191. package/src/ui/docs/content/alert-dialog.md +73 -0
  192. package/src/ui/docs/content/alert.md +69 -0
  193. package/src/ui/docs/content/app-shell.md +155 -0
  194. package/src/ui/docs/content/aspect-ratio.md +66 -0
  195. package/src/ui/docs/content/audit.md +84 -0
  196. package/src/ui/docs/content/auth.md +70 -0
  197. package/src/ui/docs/content/avatar.md +94 -0
  198. package/src/ui/docs/content/badge.md +48 -0
  199. package/src/ui/docs/content/breadcrumb.md +87 -0
  200. package/src/ui/docs/content/button-group.md +71 -0
  201. package/src/ui/docs/content/button.md +60 -0
  202. package/src/ui/docs/content/calendar.md +62 -0
  203. package/src/ui/docs/content/card.md +49 -0
  204. package/src/ui/docs/content/carousel.md +85 -0
  205. package/src/ui/docs/content/chat.md +69 -0
  206. package/src/ui/docs/content/checkbox.md +75 -0
  207. package/src/ui/docs/content/cli.md +58 -0
  208. package/src/ui/docs/content/collapsible.md +64 -0
  209. package/src/ui/docs/content/command.md +56 -0
  210. package/src/ui/docs/content/composer.md +50 -0
  211. package/src/ui/docs/content/confirm.md +120 -0
  212. package/src/ui/docs/content/copyable.md +30 -0
  213. package/src/ui/docs/content/customization.md +110 -0
  214. package/src/ui/docs/content/cycle.md +34 -0
  215. package/src/ui/docs/content/data-state.md +47 -0
  216. package/src/ui/docs/content/data.md +99 -0
  217. package/src/ui/docs/content/dialog.md +60 -0
  218. package/src/ui/docs/content/drawer.md +55 -0
  219. package/src/ui/docs/content/empty.md +66 -0
  220. package/src/ui/docs/content/events.md +61 -0
  221. package/src/ui/docs/content/field.md +58 -0
  222. package/src/ui/docs/content/getting-started.md +109 -0
  223. package/src/ui/docs/content/icon-picker.md +51 -0
  224. package/src/ui/docs/content/input-group.md +78 -0
  225. package/src/ui/docs/content/input-otp.md +72 -0
  226. package/src/ui/docs/content/input.md +78 -0
  227. package/src/ui/docs/content/item.md +84 -0
  228. package/src/ui/docs/content/kbd.md +62 -0
  229. package/src/ui/docs/content/label.md +32 -0
  230. package/src/ui/docs/content/log.md +55 -0
  231. package/src/ui/docs/content/markdown.md +41 -0
  232. package/src/ui/docs/content/mcp.md +44 -0
  233. package/src/ui/docs/content/menu.md +114 -0
  234. package/src/ui/docs/content/microcopy.md +83 -0
  235. package/src/ui/docs/content/page.md +34 -0
  236. package/src/ui/docs/content/pagination.md +99 -0
  237. package/src/ui/docs/content/popover.md +49 -0
  238. package/src/ui/docs/content/progress.md +69 -0
  239. package/src/ui/docs/content/queue.md +62 -0
  240. package/src/ui/docs/content/radio-group.md +77 -0
  241. package/src/ui/docs/content/resizable.md +86 -0
  242. package/src/ui/docs/content/router.md +56 -0
  243. package/src/ui/docs/content/runtime.md +77 -0
  244. package/src/ui/docs/content/scheduler.md +66 -0
  245. package/src/ui/docs/content/scroll-area.md +89 -0
  246. package/src/ui/docs/content/section-shell.md +121 -0
  247. package/src/ui/docs/content/select.md +342 -0
  248. package/src/ui/docs/content/separator.md +33 -0
  249. package/src/ui/docs/content/sidebar.md +38 -0
  250. package/src/ui/docs/content/skeleton.md +34 -0
  251. package/src/ui/docs/content/slider.md +64 -0
  252. package/src/ui/docs/content/spinner.md +37 -0
  253. package/src/ui/docs/content/split.md +33 -0
  254. package/src/ui/docs/content/storage.md +69 -0
  255. package/src/ui/docs/content/switch.md +69 -0
  256. package/src/ui/docs/content/table.md +102 -0
  257. package/src/ui/docs/content/tabs.md +94 -0
  258. package/src/ui/docs/content/testing.md +89 -0
  259. package/src/ui/docs/content/textarea.md +30 -0
  260. package/src/ui/docs/content/toast.md +67 -0
  261. package/src/ui/docs/content/toggle-group.md +81 -0
  262. package/src/ui/docs/content/toggle.md +72 -0
  263. package/src/ui/docs/content/tokens.md +171 -0
  264. package/src/ui/docs/content/tooltip.md +50 -0
  265. package/src/ui/docs/content/truncate.md +37 -0
  266. package/src/ui/docs/content/ui.md +40 -0
  267. package/src/ui/docs/content/upgrading.md +48 -0
  268. package/src/ui/docs/doc-client.tsx +214 -0
  269. package/src/ui/docs/doc.tsx +301 -0
  270. package/src/ui/docs/folder.tsx +149 -0
  271. package/src/ui/docs/index.ts +21 -0
  272. package/src/ui/docs/markdown.tsx +130 -0
  273. package/src/ui/docs/md-raw.d.ts +4 -0
  274. package/src/ui/docs/plugin.ts +104 -0
  275. package/src/ui/docs/registry.tsx +424 -0
  276. package/src/ui/docs/standalone.tsx +107 -0
  277. package/src/ui/drivers/react.tsx +627 -0
  278. package/src/ui/index.ts +92 -0
  279. package/src/ui/lib/cn.ts +10 -0
  280. package/src/ui/lib/zod-pt-br.ts +38 -0
  281. package/src/ui/meta.ts +412 -0
  282. package/src/ui/react.tsx +235 -0
  283. package/src/ui/router.ts +96 -0
  284. package/src/ui/theme.css +234 -0
  285. package/src/vite/design.ts +652 -0
  286. 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. |