@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,86 @@
1
+ ## Horizontal
2
+
3
+ ResizablePanelGroup envolve os painéis e o ResizableHandle entre eles. O grupo ocupa a altura do pai (h-full), então dê um tamanho ao container.
4
+
5
+ ```tsx preview col
6
+ <div className="h-48 rounded-md border">
7
+ <ResizablePanelGroup orientation="horizontal">
8
+ <ResizablePanel defaultSize={35}>
9
+ <div className="flex h-full items-center justify-center p-4 text-sm">
10
+ Árvore do repositório
11
+ </div>
12
+ </ResizablePanel>
13
+ <ResizableHandle />
14
+ <ResizablePanel defaultSize={65}>
15
+ <div className="flex h-full items-center justify-center p-4 text-sm">
16
+ Diff da sessão
17
+ </div>
18
+ </ResizablePanel>
19
+ </ResizablePanelGroup>
20
+ </div>
21
+ ```
22
+
23
+ ## Com alça visível
24
+
25
+ withHandle no ResizableHandle desenha a pega central — facilita acertar o arraste quando a divisória precisa ficar óbvia.
26
+
27
+ ```tsx preview col
28
+ <div className="h-48 rounded-md border">
29
+ <ResizablePanelGroup orientation="horizontal">
30
+ <ResizablePanel defaultSize={40} minSize={20}>
31
+ <div className="flex h-full items-center justify-center p-4 text-sm">
32
+ Painel do agente developer
33
+ </div>
34
+ </ResizablePanel>
35
+ <ResizableHandle withHandle />
36
+ <ResizablePanel defaultSize={60} minSize={20}>
37
+ <div className="flex h-full items-center justify-center p-4 text-sm">
38
+ Preview do worktree
39
+ </div>
40
+ </ResizablePanel>
41
+ </ResizablePanelGroup>
42
+ </div>
43
+ ```
44
+
45
+ ## Aninhado
46
+
47
+ Um ResizablePanelGroup dentro de um painel cruza as direções: orientation=vertical empilha os painéis e a alça vira horizontal.
48
+
49
+ ```tsx preview col
50
+ <div className="h-64 rounded-md border">
51
+ <ResizablePanelGroup orientation="horizontal">
52
+ <ResizablePanel defaultSize={30}>
53
+ <div className="flex h-full items-center justify-center p-4 text-sm">
54
+ Workspace Empresa X
55
+ </div>
56
+ </ResizablePanel>
57
+ <ResizableHandle withHandle />
58
+ <ResizablePanel defaultSize={70}>
59
+ <ResizablePanelGroup orientation="vertical">
60
+ <ResizablePanel defaultSize={60}>
61
+ <div className="flex h-full items-center justify-center p-4 text-sm">
62
+ Chat do reviewer
63
+ </div>
64
+ </ResizablePanel>
65
+ <ResizableHandle withHandle />
66
+ <ResizablePanel defaultSize={40}>
67
+ <div className="flex h-full items-center justify-center p-4 text-sm">
68
+ Saída do opus check
69
+ </div>
70
+ </ResizablePanel>
71
+ </ResizablePanelGroup>
72
+ </ResizablePanel>
73
+ </ResizablePanelGroup>
74
+ </div>
75
+ ```
76
+
77
+ ## Props
78
+
79
+ | Prop | Tipo | Default | Descrição |
80
+ |---|---|---|---|
81
+ | `orientation (ResizablePanelGroup)` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do arraste — horizontal vira colunas, vertical empilha em linhas. |
82
+ | `defaultSize (ResizablePanel)` | `number \| string` | | Tamanho inicial do painel, em porcentagem do grupo. |
83
+ | `minSize (ResizablePanel)` | `number \| string` | | Tamanho mínimo até onde o painel encolhe no arraste. |
84
+ | `maxSize (ResizablePanel)` | `number \| string` | | Tamanho máximo até onde o painel cresce no arraste. |
85
+ | `collapsible (ResizablePanel)` | `boolean` | `false` | Deixa o painel colapsar ao ser arrastado abaixo do minSize. |
86
+ | `withHandle (ResizableHandle)` | `boolean` | `false` | Desenha a pega central na divisória — torna o ponto de arraste visível. |
@@ -0,0 +1,56 @@
1
+ ## O pathname É o estado
2
+
3
+ Roteamento history-based, sem dependência: `pushState` + `popstate` + `useSyncExternalStore`. Páginas orientadas a URL — deep-link, reload e o botão voltar funcionam de graça, porque não há um segundo lugar guardando "onde estou".
4
+
5
+ ```tsx
6
+ import { navigate, useSegments } from '@softize/opus/ui/react'
7
+
8
+ function App() {
9
+ const segments = useSegments() // '/settings/users' → ['settings','users']
10
+ if (segments[0] === 'settings') return <Settings />
11
+ return <Home />
12
+ }
13
+ ```
14
+
15
+ ## O escopo é pequeno de propósito
16
+
17
+ Não há tabela de rotas, `<Route>`, params tipados nem carregamento de dados. Isto é a **leitura reativa da URL + um `navigate`** — quem decide o que renderizar é o app, com `if`/`switch` sobre os segmentos.
18
+
19
+ A régua: se a sua tela precisa de casamento de padrão (`/users/:id/posts/:postId`), params tipados ou data loaders, o caso pede uma biblioteca de rotas, não isto. Um app de back-office com uma dúzia de destinos quase nunca precisa.
20
+
21
+ ## As quatro peças
22
+
23
+ | | |
24
+ |---|---|
25
+ | `usePathname()` | O pathname como estado reativo. |
26
+ | `useSegments()` | Os segmentos, já sem os vazios: `/settings/users` → `['settings','users']`. |
27
+ | `useSearchParams()` | A querystring reativa — o lar natural do estado interno de uma seção (qual relatório está aberto, o recorte de uma lista). |
28
+ | `navigate(path, opts?)` | `pushState` + notifica. `{ replace: true }` troca a entrada corrente. |
29
+
30
+ ## Dois detalhes que custaram caro nas cópias à mão
31
+
32
+ **Destino igual é no-op.** `navigate` resolve o destino com `new URL` e compara o `href` inteiro — não faz nada se você já está exatamente lá. Sem isso, clicar duas vezes no mesmo item do menu empilha entradas idênticas e o botão "voltar" não sai do lugar.
33
+
34
+ Comparar strings cruas parece bastar e não basta, em duas frentes. O **hash**: estando em `/a#secao`, `navigate('/a')` pareceria destino repetido e a âncora nunca sairia da URL. E o **encoding**: `window.location` devolve `/relatórios` como `/relat%C3%B3rios`, então a comparação crua nunca casa e cada clique empilha — bem no caso pt-BR, e no ``navigate(`/reports?report=${arquivo}`)`` com nome de arquivo acentuado.
35
+
36
+ **A notificação é um evento, não uma lista.** `pushState` não dispara `popstate`, então `navigate` dispara — e é `window.dispatchEvent`, não um `Set` de assinantes em escopo de módulo. A diferença aparece nas bordas: código que ainda escuta `popstate` na unha continua acompanhando, e duas cópias do pacote no `node_modules` continuam se enxergando. Uma lista privada mora numa instância do bundle; o `window` é um só.
37
+
38
+ **`useSyncExternalStore`, não `useState`.** Ler `window.location` dentro de `useState`/`useEffect` sofre *tearing* no modo concurrent: dois componentes podem renderizar o mesmo commit com URLs diferentes. As cópias que este módulo substituiu faziam isso.
39
+
40
+ ## `replace`: quando o passo não é história
41
+
42
+ ```tsx
43
+ navigate(`/reports?report=${file}`, { replace: true })
44
+ ```
45
+
46
+ Use quando a mudança não merece uma volta do botão "voltar" — um rascunho que ganha id no meio do fluxo, um filtro que a pessoa ajusta várias vezes seguidas. O default (`pushState`) é o certo para navegação de verdade.
47
+
48
+ ## SSR
49
+
50
+ Os hooks têm snapshot de servidor constante (`/` e busca vazia): renderizam sem `window` e reconciliam com a URL real no primeiro render do cliente. Se a sua página precisa do path correto **no HTML servido**, isto não resolve — aí o path tem que vir do framework, por prop.
51
+
52
+ O `navigate` **não** tem essa cortesia: ele toca `window` direto e estoura no servidor. É de propósito — ler a URL sem browser tem resposta razoável, navegar não tem, e engolir a chamada em silêncio esconderia o erro de uso.
53
+
54
+ ## Destino externo não é problema daqui
55
+
56
+ `navigate` espera um caminho da própria origem. Validar destino que veio de terceiro — o `?redirect=` de um pós-login, por exemplo — é responsabilidade de quem recebe esse input, e a checagem certa é resolver com `new URL(valor, origin)` e comparar `origin`, não olhar prefixo.
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Runtime & adapters
3
+ ---
4
+
5
+ # Runtime & adapters
6
+
7
+ O Opus não é framework — é **protocolo + SDK**: você compõe o runtime a partir de
8
+ adapters plugáveis, e cada capacidade é opcional. O litmus é a inversão de controle:
9
+ o runtime não toma o seu app; o seu app monta o runtime com as peças que quer.
10
+
11
+ ## O modelo
12
+
13
+ > Todo adapter implementa `name`/`kind`; `init`/`dispose`/`healthCheck` são opcionais e
14
+ > entram no ciclo de vida do runtime (start inicializa, dispose desliga em ordem reversa,
15
+ > healthCheck agrega).
16
+
17
+ ```ts
18
+ import { createRuntime } from '@softize/opus/core'
19
+
20
+ const runtime = createRuntime({
21
+ server: fastifyServer({ app }), // monta as actions como endpoints
22
+ data: kyselyData({ db }), // fornece ctx.db
23
+ auth: makeAuthAdapter(), // resolve user/tenant/can
24
+ audit: [pgAudit({ pool })], // trilha de auditoria (N sinks)
25
+ storage: fsStorage({ root }), // fornece ctx.storage (experimental)
26
+ config: { env, i18n: { defaultLocale: 'pt-BR' } },
27
+ })
28
+
29
+ runtime.register([dealsDomain, billingDomain]) // não registrado = não existe
30
+ await runtime.start()
31
+ ```
32
+
33
+ ## As capacidades
34
+
35
+ > Um subpath por driver; quem não usa, não instala (peers opcionais onde há dependência
36
+ > pesada). A tabela é o mapa — cada linha é um recurso do protocolo.
37
+
38
+ | Capacidade | Fornece | Driver pronto |
39
+ |---|---|---|
40
+ | `server` | monta as actions (HTTP) | `fastifyServer` — `@softize/opus/server/fastify` |
41
+ | `data` | `ctx.db` + drift-check + CRUD | `kyselyData`, `kyselyRepo`, `crudActions` — `@softize/opus/data/kysely` |
42
+ | `auth` | `user`/`tenantId`/`can` do contexto | `jwtAuth` — `…/auth/jwt` · `betterAuthSession` — `…/auth/better-auth` |
43
+ | `audit` | trilha por execução de action | `pgAudit` — `…/audit/pg` · `consoleAudit` — `…/audit/console` |
44
+ | `log` | `ctx.log` estruturado | `pinoLogger` — `@softize/opus/log/pino` |
45
+ | `eventBus` | `ctx.emit` + **reactions** | `mittEvents` — `@softize/opus/events/mitt` |
46
+ | `queue` | jobs em background | `bullmqQueue` — `@softize/opus/queue/bullmq` |
47
+ | `scheduler` | **schedules** (cron/intervalo) | `nodeCronScheduler` — `@softize/opus/scheduler/node-cron` |
48
+ | `storage` | `ctx.storage` (arquivos) | `fsStorage` — `…/storage/fs` · `s3Storage` — `…/storage/s3` |
49
+ | `ai` | `ctx.ai` (complete/extract) | `anthropicAi` — `@softize/opus/ai/anthropic` |
50
+ | `client` | chamar actions de fora (stubs) | `fetchClient` — `@softize/opus/client/fetch` |
51
+
52
+ Fora do runtime, mas parte do protocolo: o harness de teste (`runAction`/`testContext`
53
+ — `@softize/opus/testing`) roda uma action em unidade com o mesmo pipeline do
54
+ `execute`. Ver **Testes de action**.
55
+
56
+ ## O contexto do handler
57
+
58
+ > O `ctx` é a soma do que os adapters fornecem — sem adapter, o campo é `null`/`unknown`
59
+ > e o handler decide como degradar.
60
+
61
+ `user`/`tenantId`/`can` (auth) · `db` (data) · `log` (logger) · `emit` (eventBus) ·
62
+ `storage` (storage) · `ai` (ai) · `provenance` (quem disparou: http, schedule,
63
+ reaction…) · `meta`.
64
+
65
+ ## Reactions e schedules
66
+
67
+ > Os dois jeitos de código rodar SEM request: evento de domínio dispara reaction;
68
+ > tempo dispara schedule (que executa uma action, com provenance própria).
69
+
70
+ Declarados como as actions e registrados no mesmo `runtime.register` — o manifest
71
+ projeta os três (o Maestro mostra o wiring em Visão geral).
72
+
73
+ ## Referência profunda
74
+
75
+ O contrato completo (tipos, garantias, ordem de execução, erros) vive no
76
+ `docs/protocol.md` **dentro do pacote** — como o changelog, viaja com a versão
77
+ que você instalou.
@@ -0,0 +1,66 @@
1
+ ---
2
+ title: Agendador
3
+ ---
4
+
5
+ # Agendador
6
+
7
+ Rodar uma action no tempo — cron ou intervalo. Você declara o schedule (qual action, quando)
8
+ e o runtime dispara na hora, com provenance `schedule` (rastreável no audit como qualquer
9
+ execução). O contrato é o `SchedulerAdapter`.
10
+
11
+ ## O contrato
12
+
13
+ ```ts
14
+ interface SchedulerAdapter {
15
+ register(spec: ScheduleDef, fire: () => Promise<void> | void): Unsubscribe
16
+ }
17
+
18
+ interface ScheduleDef {
19
+ name: string
20
+ action: string // action do registry, executada quando dispara
21
+ cron?: string // '0 9 * * *' (9h todo dia)
22
+ every?: string // '1h', '30m', '15s' — atalho de intervalo
23
+ timezone?: string // IANA, ex.: 'America/Sao_Paulo' (default UTC)
24
+ input?: unknown | (() => unknown | Promise<unknown>) // estático ou dinâmico
25
+ enabled?: boolean
26
+ }
27
+ ```
28
+
29
+ O timing é exclusivo: `cron` **ou** `every`. A action referenciada tem que existir no
30
+ registry.
31
+
32
+ ## Driver
33
+
34
+ ```ts
35
+ import { nodeCronScheduler } from '@softize/opus/scheduler/node-cron'
36
+
37
+ const scheduler = nodeCronScheduler() // in-process
38
+ ```
39
+
40
+ ## No runtime
41
+
42
+ ```ts
43
+ import { defineSchedule } from '@softize/opus/core'
44
+
45
+ // Declarado no domínio, junto das actions:
46
+ const fecharDia = defineSchedule({
47
+ name: 'fechar-dia',
48
+ action: 'caixa.fechar',
49
+ cron: '0 23 * * *',
50
+ timezone: 'America/Sao_Paulo',
51
+ })
52
+
53
+ const runtime = createRuntime({
54
+ // …server/data…
55
+ scheduler,
56
+ })
57
+ ```
58
+
59
+ O runtime registra os schedules do domínio no `start()` e chama a action quando o tempo bate
60
+ — nada de disparar no handler.
61
+
62
+ ## Limites (por enquanto)
63
+
64
+ Driver hoje: `node-cron` (in-process — some se o processo cair; num cluster, cada nó
65
+ dispararia). Backends duráveis/distribuídos (BullMQ repeatable, Temporal…) entram por
66
+ reincidência.
@@ -0,0 +1,89 @@
1
+ ## Lista de altura fixa
2
+
3
+ Dê a altura ao ScrollArea (h-48) e ponha o conteúdo dentro — a barra vertical já vem por padrão, sem precisar de ScrollBar.
4
+
5
+ ```tsx preview col
6
+ const sessions = [
7
+ { id: 'empresa-x-1842', title: 'Migrar billing pro novo schema', agent: 'developer' },
8
+ { id: 'empresa-x-1839', title: 'Revisar handoff do checkout', agent: 'reviewer' },
9
+ { id: 'empresa-x-1835', title: 'Redesenhar o painel de rotas', agent: 'designer' },
10
+ { id: 'empresa-x-1830', title: 'Corrigir flaky no teste de webhook', agent: 'developer' },
11
+ { id: 'empresa-x-1827', title: 'Vincular skill clean-code ao agente', agent: 'reviewer' },
12
+ { id: 'empresa-x-1821', title: 'Subir preview do worktree', agent: 'developer' },
13
+ { id: 'empresa-x-1818', title: 'Ajustar copy do empty state', agent: 'designer' },
14
+ { id: 'empresa-x-1814', title: 'Rodar opus check no monorepo', agent: 'reviewer' },
15
+ ]
16
+
17
+ render(
18
+ <ScrollArea className="h-48 w-full rounded-md border border-border">
19
+ <div className="p-3">
20
+ {sessions.map((s) => (
21
+ <div key={s.id} className="rounded-md px-2 py-1.5 text-sm hover:bg-muted">
22
+ <p className="font-medium">{s.title}</p>
23
+ <p className="text-xs text-muted-foreground">{s.id} · {s.agent}</p>
24
+ </div>
25
+ ))}
26
+ </div>
27
+ </ScrollArea>,
28
+ )
29
+ ```
30
+
31
+ ## Trilho horizontal
32
+
33
+ Pra rolar na horizontal, acrescente <ScrollBar orientation="horizontal" /> como filho e deixe o conteúdo numa linha que não quebra (flex + w-max).
34
+
35
+ ```tsx preview col
36
+ const skills = ['clean-code', 'code-style', 'ddd', 'test', 'create-action', 'opus', 'clean-architecture']
37
+
38
+ render(
39
+ <ScrollArea className="w-full rounded-md border border-border whitespace-nowrap">
40
+ <div className="flex w-max gap-2 p-3">
41
+ {skills.map((skill) => (
42
+ <span key={skill} className="rounded-md border border-border bg-muted px-3 py-1.5 text-sm">
43
+ {skill}
44
+ </span>
45
+ ))}
46
+ </div>
47
+ <ScrollBar orientation="horizontal" />
48
+ </ScrollArea>,
49
+ )
50
+ ```
51
+
52
+ ## Painel com divisórias
53
+
54
+ O conteúdo é livre: aqui o log do agente, com Separator entre as linhas. A barra só aparece quando o conteúdo passa da altura.
55
+
56
+ ```tsx preview col
57
+ const log = [
58
+ '12:04 developer clonou o worktree de empresa-x-api',
59
+ '12:05 developer rodou pnpm install',
60
+ '12:07 developer abriu a sessão empresa-x-1842',
61
+ '12:11 developer aplicou o diff no schema de billing',
62
+ '12:14 reviewer foi acionado no handoff',
63
+ '12:16 reviewer apontou 2 ajustes de microcopy',
64
+ '12:19 developer corrigiu a microcopy',
65
+ '12:22 opus check passou em todos os pacotes',
66
+ '12:24 reviewer aprovou o handoff',
67
+ ]
68
+
69
+ render(
70
+ <ScrollArea className="h-40 w-full rounded-md border border-border">
71
+ <div className="p-3 text-sm">
72
+ {log.map((line, i) => (
73
+ <div key={i}>
74
+ {i > 0 && <Separator className="my-1.5" />}
75
+ <p className="text-muted-foreground">{line}</p>
76
+ </div>
77
+ ))}
78
+ </div>
79
+ </ScrollArea>,
80
+ )
81
+ ```
82
+
83
+ ## Props
84
+
85
+ | Prop | Tipo | Default | Descrição |
86
+ |---|---|---|---|
87
+ | `className (ScrollArea)` | `string` | | Onde a altura (h-48) ou a largura mora — é o que define a janela rolável. Sem dimensão, não há o que rolar. |
88
+ | `children (ScrollArea)` | `React.ReactNode` | | O conteúdo da janela. Inclua um <ScrollBar orientation="horizontal" /> entre os filhos pra habilitar a rolagem lateral. |
89
+ | `orientation (ScrollBar)` | `'vertical' \| 'horizontal'` | `'vertical'` | A direção da barra. A vertical já vem embutida; adicione a horizontal só quando precisar. |
@@ -0,0 +1,121 @@
1
+ ## Seção com navegação própria
2
+
3
+ O nível do meio do chrome. O `AppShell` é o quadro do app e o `Page` é o esqueleto de **uma** tela; entre os dois falta a **seção**: um grupo de telas irmãs (Configurações, Relatórios, a doc) que quer a lista **dentro** do conteúdo, não pendurada na sidebar do app.
4
+
5
+ A nav é `w-56` — mais estreita que o `w-64` do `AppShell` de propósito, pra hierarquia ficar legível quando as duas aparecem lado a lado.
6
+
7
+ ```tsx preview col
8
+ render(
9
+ <div className="h-96 w-full overflow-hidden rounded-lg border border-border">
10
+ <SectionShell
11
+ groups={[
12
+ { label: 'Acesso', items: [{ id: 'users', label: 'Usuários' }, { id: 'roles', label: 'Perfis' }] },
13
+ { label: 'Cadastros', items: [{ id: 'units', label: 'Unidades' }, { id: 'departments', label: 'Departamentos' }] },
14
+ { items: [{ id: 'audit', label: 'Auditoria' }, { id: 'integrations', label: 'Integrações', disabled: true }] },
15
+ ]}
16
+ activeId="users"
17
+ onSelect={() => {}}
18
+ >
19
+ <Page title="Usuários" description="As contas do IdP.">
20
+ <div className="rounded-lg border border-dashed border-border p-10 text-center text-sm text-muted-foreground">
21
+ A tela do item ativo (em geral um Page).
22
+ </div>
23
+ </Page>
24
+ </SectionShell>
25
+ </div>,
26
+ )
27
+ ```
28
+
29
+ ## Quando usar — sidebar, tabs ou seção
30
+
31
+ A regra da casa, pra não virar gosto pessoal:
32
+
33
+ - **Sidebar do app** (`AppShell`) — os **contextos de negócio**, o mapa que a pessoa carrega na cabeça o dia todo. Custa caro: cada item aqui compete com todos os outros.
34
+ - **`SectionShell`** — as telas de uma seção que a pessoa **visita raro e navega por dentro** quando visita. Configurações é o caso canônico: nenhum admin quer 6 itens de config disputando espaço com Vendas e Estoque. Também é a escolha quando a lista é **dinâmica** (relatórios, documentos) e não cabe num nav estático.
35
+ - **Tabs** — quando são **facetas do mesmo objeto** (a mesma entidade vista de ângulos diferentes), não telas distintas. Se cada aba tem URL própria e faz sentido dar deep-link, provavelmente é seção, não aba.
36
+
37
+ Sintoma de que uma seção devia sair da sidebar: o item de topo **não tem tela própria** e só existe pra abrir um accordion. Isso é um nó de menu, não um destino — e cobra o preço de um destino.
38
+
39
+ ## Roteamento fica de fora
40
+
41
+ O componente é **controlado**: `activeId` + `onSelect`. Ele não lê nem escreve URL — quem roteia é o app, com o mecanismo que já usa (path, querystring, estado). Mesma decisão do `AppShell`: o shell é o quadro, não o conteúdo.
42
+
43
+ O padrão comum é casar `activeId` com um segmento do path e navegar no `onSelect`:
44
+
45
+ ```tsx
46
+ <SectionShell
47
+ groups={groups}
48
+ activeId={segments[1]} // /settings/users → 'users'
49
+ onSelect={(id) => navigate(`/settings/${id}`)}
50
+ >
51
+ {screen}
52
+ </SectionShell>
53
+ ```
54
+
55
+ ## O painel remonta na troca
56
+
57
+ Trocar de item **remonta** o painel — é isso que zera o scroll (sem o remount, a tela nova abre na altura em que a anterior estava). É o detalhe que toda cópia à mão esquecia.
58
+
59
+ A chave do remount é o `activeId`. `scrollResetKey` sobrescreve, e há dois usos legítimos — em direções opostas:
60
+
61
+ ```tsx
62
+ // NUNCA remontar: a seção preserva o scroll ao trocar de item (chave constante).
63
+ <SectionShell activeId={id} scrollResetKey="fixa" …>
64
+
65
+ // Remontar MAIS: também dentro da mesma tela, quando a sub-rota troca de registro.
66
+ <SectionShell activeId={id} scrollResetKey={`${id}/${recordId}`} …>
67
+ ```
68
+
69
+ Passar `scrollResetKey={id}` com o mesmo valor do `activeId` é o default escrito à mão — não faz nada.
70
+
71
+ ## Nav redimensionável
72
+
73
+ `resizable` põe uma divisa arrastável entre a nav e o painel — o mesmo mecanismo do `rail` do AppShell, agora no terceiro nível do chrome. Útil quando os rótulos da nav variam de tamanho (nome de sessão, de arquivo) e o `w-56` fixo ora aperta, ora sobra. Ligado, a largura passa a ser % (`navDefaultSize`/`navMinSize`); desligado, segue a coluna fixa de sempre.
74
+
75
+ ```tsx preview
76
+ <div className="h-64 overflow-hidden rounded-md border">
77
+ <SectionShell
78
+ resizable
79
+ navDefaultSize={30}
80
+ groups={[{ items: [{ id: 'acoes', label: 'Ações' }, { id: 'entidades', label: 'Entidades' }] }]}
81
+ activeId="acoes"
82
+ onSelect={() => {}}
83
+ >
84
+ <div className="p-4 text-sm text-muted-foreground">Arraste a divisa à esquerda.</div>
85
+ </SectionShell>
86
+ </div>
87
+ ```
88
+
89
+ ## Três níveis, quando precisar
90
+
91
+ A maioria das seções é lista simples (`items` no grupo). Quando um grupo tem famílias internas — sub-pasta na doc, por exemplo — use `subgroups`: eles saem **depois** dos `items` soltos, com rótulo mais fraco e mais indentado.
92
+
93
+ ```tsx
94
+ groups={[
95
+ {
96
+ label: 'Guias',
97
+ items: [{ id: 'deploy', label: 'Deploy' }], // solto na seção
98
+ subgroups: [{ label: 'CI', items: [{ id: 'build', label: 'Build' }] }],
99
+ },
100
+ ]}
101
+ ```
102
+
103
+ Rótulo vazio (`''`) conta como ausente — quem monta a lista a partir de dados não precisa normalizar antes.
104
+
105
+ ## Props
106
+
107
+ | Prop | Tipo | O que faz |
108
+ |---|---|---|
109
+ | `groups` | `SectionNavGroup[]` | A nav: grupos (`label` opcional) → `items` e/ou `subgroups`. |
110
+ | `activeId` | `string` | O item destacado (ganha `aria-current="page"`). Ausente = nenhum. |
111
+ | `onSelect` | `(id: string) => void` | Clique no item. |
112
+ | `navHeader` / `navFooter` | `ReactNode` | Topo e rodapé da nav (título da seção, busca, botão de criar). |
113
+ | `navLabel` | `string` | Rótulo da landmark `<nav>`. Default: `Navegação da seção`. |
114
+ | `scrollResetKey` | `string` | O que remonta o painel. Default: `activeId`. |
115
+ | `resizable` | `boolean` | Nav arrastável pelo usuário (mesma divisa do `rail` do AppShell). Default: `false` — a nav é a coluna fixa `w-56`. |
116
+ | `navDefaultSize` / `navMinSize` | `number` | Largura inicial e mínima da nav em %, quando `resizable`. Default: `18` e `12`. |
117
+ | `className` / `navClassName` | `string` | Classes da raiz e da COLUNA da nav (o wrapper, não a `<nav>` de dentro). Com `resizable`, largura em `navClassName` não vale — quem manda é `navDefaultSize`. |
118
+
119
+ Cada `SectionNavItem` tem `id`, `label` e, opcionais, `icon`, `badge` e `disabled` (visível mas inerte — a tela existe no mapa e ainda não abre).
120
+
121
+ O `id` precisa ser único em **toda** a nav, não só dentro do grupo: o `activeId` casa por id, então id repetido marca dois itens como atuais e o painel não remonta ao alternar entre eles.