@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,342 @@
1
+ ## Básico
2
+
3
+ Um componente para toda escolha em lista. As opções são DADO (`options`), não JSX: `{ value, label }` — o `label` é o que o campo mostra, o que a busca casa e o que o leitor de tela lê. O `id` pareia com o `htmlFor` do Label.
4
+
5
+ ```tsx preview col md
6
+ const [role, setRole] = useState('')
7
+
8
+ render(
9
+ <div className="grid gap-2">
10
+ <Label htmlFor="agent-role">Papel do agente</Label>
11
+ <Select
12
+ id="agent-role"
13
+ value={role}
14
+ onChange={setRole}
15
+ placeholder="Selecione o papel"
16
+ options={[
17
+ { value: 'designer', label: 'Designer' },
18
+ { value: 'developer', label: 'Developer' },
19
+ { value: 'reviewer', label: 'Revisor' },
20
+ ]}
21
+ />
22
+ </div>,
23
+ )
24
+ ```
25
+
26
+ ## Com busca (searchable)
27
+
28
+ `searchable` transforma o próprio campo na busca: digitou, a lista filtra embaixo (fuzzy do cmdk, casando label, hint e value). É o que resolve a **achabilidade** — a régua é essa, não performance: 200 itens sem busca é embaçado, achar é rolar até cansar.
29
+
30
+ ```tsx preview col md
31
+ const [issue, setIssue] = useState('')
32
+
33
+ render(
34
+ <div className="grid gap-2">
35
+ <Label htmlFor="issue">Issue da sessão</Label>
36
+ <Select
37
+ searchable
38
+ id="issue"
39
+ value={issue}
40
+ onChange={setIssue}
41
+ placeholder="Buscar issue…"
42
+ options={[
43
+ { value: '412', label: 'Ajustar microcopy do handoff', hint: 'SOF-412' },
44
+ { value: '418', label: 'Preview da sessão cai após deploy', hint: 'SOF-418' },
45
+ { value: '425', label: 'Sincronizar skills com o registry', hint: 'SOF-425' },
46
+ ]}
47
+ />
48
+ </div>,
49
+ )
50
+ ```
51
+
52
+ ## Múltiplo (chips)
53
+
54
+ `multiple` muda o contrato (`value` e `onChange` viram `string[]`): os escolhidos viram chips removíveis e a lista fica aberta pra seleção em sequência. Com a busca vazia, o topo do dropdown oferece Selecionar tudo / Limpar seleção (só no modo client — com filtro ativo ou `onSearch`, "tudo" seria ambíguo).
55
+
56
+ ```tsx preview col md
57
+ const [skills, setSkills] = useState<string[]>(['clean-code'])
58
+
59
+ render(
60
+ <div className="grid gap-2">
61
+ <Label htmlFor="agent-skills">Skills do agente</Label>
62
+ <Select
63
+ multiple
64
+ searchable
65
+ id="agent-skills"
66
+ value={skills}
67
+ onChange={setSkills}
68
+ placeholder="Adicionar skill…"
69
+ options={[
70
+ { value: 'clean-code', label: 'clean-code' },
71
+ { value: 'code-style', label: 'code-style' },
72
+ { value: 'ddd', label: 'ddd' },
73
+ { value: 'test', label: 'test' },
74
+ ]}
75
+ />
76
+ </div>,
77
+ )
78
+ ```
79
+
80
+ ## Nativo (native)
81
+
82
+ `native` renderiza o `<select>` do sistema (só com a seta restilizada — a nativa cola na borda e ignora o tema). Ganha o picker do próprio SO: roda de scroll no iOS, teclado do Android, zero JS. Use em **lista curta e sabida** (2–8 itens que a pessoa já conhece: Sim/Não, prioridade, ambiente). Item rico, busca ou multi não existem aqui — pra isso, o modo default.
83
+
84
+ ```tsx preview col md
85
+ const [env, setEnv] = useState('preview')
86
+
87
+ render(
88
+ <div className="grid gap-2">
89
+ <Label htmlFor="env">Ambiente</Label>
90
+ <Select
91
+ native
92
+ id="env"
93
+ value={env}
94
+ onChange={setEnv}
95
+ options={[
96
+ { value: 'dev', label: 'Dev local' },
97
+ { value: 'preview', label: 'Preview da sessão' },
98
+ { value: 'prod', label: 'Produção' },
99
+ ]}
100
+ />
101
+ </div>,
102
+ )
103
+ ```
104
+
105
+ ## Grupos
106
+
107
+ `group` na opção agrupa a lista — bloco nomeado nos dois modos (`<optgroup>` no nativo, heading no custom). Pra listas com origens distintas (membros × agentes).
108
+
109
+ ```tsx preview col md
110
+ const [reviewer, setReviewer] = useState('')
111
+
112
+ render(
113
+ <Select
114
+ value={reviewer}
115
+ onChange={setReviewer}
116
+ placeholder="Escolha o revisor"
117
+ options={[
118
+ { value: 'joao', label: 'João Campos', group: 'Membros' },
119
+ { value: 'rafa', label: 'Rafa', group: 'Membros' },
120
+ { value: 'developer', label: 'Developer', group: 'Agentes' },
121
+ { value: 'reviewer', label: 'Revisor', group: 'Agentes' },
122
+ ]}
123
+ />,
124
+ )
125
+ ```
126
+
127
+ ## Busca server-side
128
+
129
+ Com `onSearch` o filtro do cmdk desliga: o pai busca (debounced, ao abrir e ao digitar) e devolve `options`; `loading` mostra Buscando… enquanto o fetch corre. Implica campo buscável (não precisa repetir `searchable`).
130
+
131
+ ```tsx preview col md
132
+ const ASSIGNEES = [
133
+ { value: 'joao', label: 'João Campos', hint: 'humano' },
134
+ { value: 'designer', label: 'Designer', hint: 'IA' },
135
+ { value: 'developer', label: 'Developer', hint: 'IA' },
136
+ { value: 'reviewer', label: 'Revisor', hint: 'IA' },
137
+ ]
138
+
139
+ const [assignee, setAssignee] = useState('')
140
+ const [results, setResults] = useState(ASSIGNEES)
141
+ const [loading, setLoading] = useState(false)
142
+
143
+ // Simula a busca no servidor — no app real, aqui vai o fetch.
144
+ const searchAssignees = (query: string): void => {
145
+ setLoading(true)
146
+ setTimeout(() => {
147
+ setResults(ASSIGNEES.filter((a) => a.label.toLowerCase().includes(query.toLowerCase())))
148
+ setLoading(false)
149
+ }, 400)
150
+ }
151
+
152
+ render(
153
+ <Select
154
+ value={assignee}
155
+ onChange={setAssignee}
156
+ options={results}
157
+ onSearch={searchAssignees}
158
+ loading={loading}
159
+ searchPlaceholder="Buscar responsável…"
160
+ />,
161
+ )
162
+ ```
163
+
164
+ ## Ícone dentro do campo
165
+
166
+ `icon` põe o ícone DENTRO do controle, antes do texto (e dos chips, no multi) — identifica o campo numa barra de filtros. Ícone AO LADO do campo é o antipadrão: desalinha na quebra de linha e some no responsivo. Vale nos dois modos.
167
+
168
+ ```tsx preview col-start
169
+ const [task, setTask] = useState('')
170
+
171
+ render(
172
+ <Select
173
+ icon={<Ticket />}
174
+ value={task}
175
+ onChange={setTask}
176
+ placeholder="Vincular task"
177
+ options={[
178
+ { value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' },
179
+ { value: 'gra-7', label: 'Filtro por filial', hint: 'GRA-7' },
180
+ ]}
181
+ />,
182
+ )
183
+ ```
184
+
185
+ ## Densidade (size)
186
+
187
+ O controle segue a altura do design system: default é h-9 (a mesma do Input e do Button) e `size="sm"` dá h-8 pra barra de filtros/toolbar densa.
188
+
189
+ ```tsx preview col-start
190
+ const [a, setA] = useState('gra-2')
191
+
192
+ render(
193
+ <div className="flex items-center gap-2">
194
+ <Select
195
+ size="sm"
196
+ icon={<Ticket />}
197
+ value={a}
198
+ onChange={setA}
199
+ options={[{ value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' }]}
200
+ />
201
+ <Button size="sm" variant="outline">Filtrar</Button>
202
+ </div>,
203
+ )
204
+ ```
205
+
206
+ ## Limpar (clearable)
207
+
208
+ `clearable` põe o X no controle quando há seleção — limpa num clique (single volta a vazio, multi a `[]`), sem abrir a lista. Bom pra filtro, onde "sem valor" é um estado de uso frequente; os filtros do ActionList já vêm assim.
209
+
210
+ ```tsx preview col md
211
+ function Demo() {
212
+ const [values, setValues] = useState(['developer', 'reviewer'])
213
+ return (
214
+ <Select
215
+ multiple
216
+ clearable
217
+ value={values}
218
+ onChange={setValues}
219
+ options={[
220
+ { value: 'developer', label: 'Developer' },
221
+ { value: 'reviewer', label: 'Reviewer' },
222
+ { value: 'designer', label: 'Designer' },
223
+ ]}
224
+ placeholder="Papéis…"
225
+ className="w-72"
226
+ />
227
+ )
228
+ }
229
+ render(<Demo />)
230
+ ```
231
+
232
+ ## Item rico (content)
233
+
234
+ `label` é sempre string (é o que o campo mostra e a busca casa); `content` é o render RICO do item na lista — ícone, avatar, duas linhas. Assim o item pode ser elaborado sem estragar busca nem a11y.
235
+
236
+ ```tsx preview col md
237
+ const [agent, setAgent] = useState('developer')
238
+
239
+ render(
240
+ <Select
241
+ value={agent}
242
+ onChange={setAgent}
243
+ className="w-72"
244
+ options={[
245
+ {
246
+ value: 'developer',
247
+ label: 'Developer',
248
+ content: (
249
+ <span className="flex items-center gap-2">
250
+ <Bot className="size-4 text-muted-foreground" />
251
+ <span>
252
+ Developer <span className="text-muted-foreground">· implementa</span>
253
+ </span>
254
+ </span>
255
+ ),
256
+ },
257
+ {
258
+ value: 'reviewer',
259
+ label: 'Revisor',
260
+ content: (
261
+ <span className="flex items-center gap-2">
262
+ <ShieldCheck className="size-4 text-muted-foreground" />
263
+ <span>
264
+ Revisor <span className="text-muted-foreground">· contesta</span>
265
+ </span>
266
+ </span>
267
+ ),
268
+ },
269
+ ]}
270
+ />,
271
+ )
272
+ ```
273
+
274
+ ## Barra do composer (variant ghost)
275
+
276
+ `variant="ghost"` tira moldura e padding: o seletor discreto que vive na barra de ações do Composer/Chat (app, task, agente), à esquerda do enviar. `triggerLabel` na opção encurta o gatilho quando o rótulo da lista é longo (o título inteiro fica só na lista).
277
+
278
+ ```tsx preview col-start
279
+ const [task, setTask] = useState('GB-42')
280
+
281
+ render(
282
+ <Select
283
+ variant="ghost"
284
+ value={task}
285
+ onChange={setTask}
286
+ options={[
287
+ { value: '', label: 'Sem task' },
288
+ { value: 'GB-42', label: 'Trocar o FAB por botão no header', hint: 'GB-42', triggerLabel: 'GB-42' },
289
+ { value: 'GB-51', label: 'Anexo no composer de criação', hint: 'GB-51', triggerLabel: 'GB-51' },
290
+ ]}
291
+ />,
292
+ )
293
+ ```
294
+
295
+ ## Ação no fim do campo (trailing)
296
+
297
+ `trailing` põe uma ação custom DENTRO do controle, no fim (antes do chevron) — um botão que age sobre o valor escolhido, sem virar um irmão solto ao lado do campo. O clique no trailing **não** abre a lista (o slot para a propagação).
298
+
299
+ ```tsx preview col-start
300
+ const [task, setTask] = useState('gra-2')
301
+
302
+ render(
303
+ <Select
304
+ icon={<Ticket />}
305
+ value={task}
306
+ onChange={setTask}
307
+ className="w-72"
308
+ options={[
309
+ { value: 'gra-2', label: 'Nova tela anotações', hint: 'GRA-2' },
310
+ { value: 'gra-7', label: 'Filtro por filial', hint: 'GRA-7' },
311
+ ]}
312
+ trailing={
313
+ <Button variant="ghost" size="icon-xs">
314
+ <FileText />
315
+ </Button>
316
+ }
317
+ />,
318
+ )
319
+ ```
320
+
321
+ ## Props
322
+
323
+ | Prop | Tipo | Default | Descrição |
324
+ |---|---|---|---|
325
+ | `options` | `SelectOption[]` | | As opções: `{ value, label, hint?, content?, triggerLabel?, group?, disabled? }`. |
326
+ | `value` | `string \| string[]` | | O selecionado: string no single, string[] no multiple. |
327
+ | `onChange` | `(value: string) => void \| (value: string[]) => void` | | Chamado ao escolher (e ao remover chip, no multiple) — a assinatura segue o modo. |
328
+ | `native` | `boolean` | `false` | Renderiza o `<select>` do sistema. Exclui busca, multi e ghost (o browser é quem desenha a lista). |
329
+ | `searchable` | `boolean` | `false` | O campo vira busca: filtra a lista enquanto digita. |
330
+ | `multiple` | `boolean` | `false` | Chips removíveis, lista que permanece aberta e Selecionar tudo. |
331
+ | `variant` | `'default' \| 'ghost'` | `'default'` | `ghost` = sem moldura, pra barra do composer. |
332
+ | `placeholder` | `string` | `'Selecione…'` | Texto do campo vazio. |
333
+ | `searchPlaceholder` | `string` | | Placeholder enquanto busca; cai pro `placeholder` se ausente. |
334
+ | `emptyText` | `string` | `'Nada encontrado.'` | Mensagem quando a busca não acha nada. |
335
+ | `onSearch` | `(query: string) => void` | | Busca server-side (debounced, ao abrir e ao digitar): desliga o filtro do cmdk — o pai atualiza `options`. |
336
+ | `loading` | `boolean` | | Mostra Buscando… enquanto o fetch corre (use com `onSearch`). |
337
+ | `clearable` | `boolean` | `false` | X no controle quando há seleção — limpa num clique. |
338
+ | `icon` | `React.ReactNode` | | Ícone leading DENTRO do controle (decorativo) — herda `size-4` e o tom muted. |
339
+ | `trailing` | `React.ReactNode` | | Ação custom no FIM do controle (antes do chevron) — o clique não abre a lista. |
340
+ | `size` | `'default' \| 'sm'` | `'default'` | Altura: default (h-9, a do Input e do Button) ou sm (h-8) pra toolbar densa. |
341
+ | `disabled` | `boolean` | `false` | Esmaece e trava o controle. |
342
+ | `id` | `string` | | Vai pro campo — pra parear com o `htmlFor` do Label. |
@@ -0,0 +1,33 @@
1
+ ## Horizontal
2
+
3
+ Divide blocos empilhados — ocupa a largura toda do contêiner.
4
+
5
+ ```tsx preview col
6
+ <div>
7
+ <h4 className="text-sm font-medium">Empresa X</h4>
8
+ <p className="text-sm text-muted-foreground">Workspace do cliente Empresa X.</p>
9
+ </div>
10
+ <Separator />
11
+ <p className="text-sm text-muted-foreground">Última sessão há 2 horas.</p>
12
+ ```
13
+
14
+ ## Vertical
15
+
16
+ Entre itens de uma linha. O vertical herda a altura do contêiner — dê altura à linha (ex.: h-4 no flex).
17
+
18
+ ```tsx preview
19
+ <div className="flex h-4 items-center gap-3 text-sm">
20
+ <span>2 repositórios</span>
21
+ <Separator orientation="vertical" />
22
+ <span>3 agentes</span>
23
+ <Separator orientation="vertical" />
24
+ <span>6 skills</span>
25
+ </div>
26
+ ```
27
+
28
+ ## Props
29
+
30
+ | Prop | Tipo | Default | Descrição |
31
+ |---|---|---|---|
32
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção da linha. Vertical precisa de altura vinda do contêiner. |
33
+ | `decorative` | `boolean` | `true` | Decorativa some da árvore de acessibilidade. Use false quando a divisão é semântica. |
@@ -0,0 +1,38 @@
1
+ ## Sidebar
2
+
3
+ `Sidebar` é uma coluna de chrome e navegação. Ela não escolhe a posição: entra em qualquer `Pane` de um `Split`. A mesma forma serve para a sidebar global do app e a navegação interna de Configurações.
4
+
5
+ ```tsx preview
6
+ render(
7
+ <div className="h-72 overflow-hidden rounded-lg border border-border">
8
+ <Split resizable>
9
+ <Pane initialSize={32} minSize={20} inset="none">
10
+ <Sidebar divider={false} className="w-full">
11
+ <PaneHeader className="px-4 py-3 text-sm font-semibold">Projeto</PaneHeader>
12
+ <PaneContent>
13
+ <SidebarNav
14
+ groups={[{ label: 'Design', items: [{ id: 'preview', label: 'Preview' }, { id: 'code', label: 'Código' }] }]}
15
+ activeId="preview"
16
+ onSelect={() => {}}
17
+ />
18
+ </PaneContent>
19
+ <PaneFooter><span className="text-xs text-muted-foreground">Configurações</span></PaneFooter>
20
+ </Sidebar>
21
+ </Pane>
22
+ <Pane grow inset="lg"><p className="text-sm text-muted-foreground">Conteúdo da tela.</p></Pane>
23
+ </Split>
24
+ </div>,
25
+ )
26
+ ```
27
+
28
+ ## Colapso
29
+
30
+ `collapsed` pertence à própria `Sidebar`; `SidebarItem` e `SidebarNav` adaptam-se automaticamente para botões `size-9` centralizados, ícones e tooltips. `PaneHeader`, `PaneContent` e `PaneFooter` são slots do pane: a aplicação mantém a identidade e ações que lhe pertencem sem atribuí-las artificialmente à sidebar. `SidebarHeader`, `SidebarContent` e `SidebarFooter` seguem como aliases deprecated durante a migração.
31
+
32
+ ```tsx
33
+ <Sidebar collapsed={collapsed}>
34
+ <PaneHeader><MySidebarHeader onToggle={() => setCollapsed(!collapsed)} /></PaneHeader>
35
+ <PaneContent><SidebarNav groups={groups} activeId={active} onSelect={go} /></PaneContent>
36
+ <PaneFooter><UserMenu /></PaneFooter>
37
+ </Sidebar>
38
+ ```
@@ -0,0 +1,34 @@
1
+ ## Linha de lista com avatar
2
+
3
+ O skeleton imita a forma do conteúdo: círculo pro avatar (rounded-full), barras com a largura aproximada do texto.
4
+
5
+ ```tsx preview col
6
+ <div className="flex items-center gap-3">
7
+ <Skeleton className="size-10 rounded-full" />
8
+ <div className="space-y-2">
9
+ <Skeleton className="h-4 w-48" />
10
+ <Skeleton className="h-3 w-32" />
11
+ </div>
12
+ </div>
13
+ ```
14
+
15
+ ## Card em carregamento
16
+
17
+ O esqueleto reproduz o layout final do card — título, descrição, conteúdo e ações — pra tela não pular quando os dados chegarem.
18
+
19
+ ```tsx preview col
20
+ <Card>
21
+ <CardHeader>
22
+ <Skeleton className="h-5 w-40" />
23
+ <Skeleton className="h-4 w-56" />
24
+ </CardHeader>
25
+ <CardContent className="space-y-2">
26
+ <Skeleton className="h-4 w-full" />
27
+ <Skeleton className="h-4 w-3/4" />
28
+ </CardContent>
29
+ <CardFooter className="gap-2">
30
+ <Skeleton className="h-8 w-28" />
31
+ <Skeleton className="h-8 w-24" />
32
+ </CardFooter>
33
+ </Card>
34
+ ```
@@ -0,0 +1,64 @@
1
+ ## Básico
2
+
3
+ defaultValue é um array — [n] pra um thumb. min, max e step delimitam a faixa; aqui de 0 a 100 em passos de 5.
4
+
5
+ ```tsx preview col
6
+ <Slider defaultValue={[40]} min={0} max={100} step={5} />
7
+ ```
8
+
9
+ ## Controlado
10
+
11
+ onValueChange recebe o array atualizado — leia value[0] pra um thumb. Bom pra mostrar o valor escolhido ao lado do rótulo.
12
+
13
+ ```tsx preview col
14
+ const [parallelism, setParallelism] = useState([4])
15
+
16
+ render(
17
+ <div className="grid w-full gap-3">
18
+ <div className="flex items-center justify-between">
19
+ <Label>Sessões em paralelo</Label>
20
+ <span className="text-sm text-muted-foreground">{parallelism[0]}</span>
21
+ </div>
22
+ <Slider value={parallelism} onValueChange={setParallelism} min={1} max={8} step={1} />
23
+ </div>,
24
+ )
25
+ ```
26
+
27
+ ## Intervalo
28
+
29
+ value com dois números rende dois thumbs e a faixa fica entre eles — o jeito de filtrar por um intervalo, como o custo estimado de uma task.
30
+
31
+ ```tsx preview col
32
+ const [budget, setBudget] = useState([20, 60])
33
+
34
+ render(
35
+ <div className="grid w-full gap-3">
36
+ <div className="flex items-center justify-between">
37
+ <Label>Custo estimado (US$)</Label>
38
+ <span className="text-sm text-muted-foreground">{budget[0]} – {budget[1]}</span>
39
+ </div>
40
+ <Slider value={budget} onValueChange={setBudget} min={0} max={100} step={5} />
41
+ </div>,
42
+ )
43
+ ```
44
+
45
+ ## Desabilitado
46
+
47
+ disabled esmaece o trilho e o thumb e bloqueia o arraste — o valor fica congelado no defaultValue.
48
+
49
+ ```tsx preview col
50
+ <Slider defaultValue={[70]} min={0} max={100} disabled />
51
+ ```
52
+
53
+ ## Props
54
+
55
+ | Prop | Tipo | Default | Descrição |
56
+ |---|---|---|---|
57
+ | `value` | `number[]` | | O valor no modo controlado — [n] pra um thumb, [a, b] pra um intervalo. Pareie com onValueChange. |
58
+ | `onValueChange` | `(value: number[]) => void` | | Chamado a cada arraste, com o array atualizado. |
59
+ | `defaultValue` | `number[]` | | Valor inicial no modo não controlado. |
60
+ | `min` | `number` | `0` | Limite inferior da faixa. |
61
+ | `max` | `number` | `100` | Limite superior da faixa. |
62
+ | `step` | `number` | `1` | Incremento entre valores possíveis. |
63
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direção do trilho — vertical exige altura no container. |
64
+ | `disabled` | `boolean` | `false` | Esmaece e bloqueia o arraste. |
@@ -0,0 +1,37 @@
1
+ ## Tamanhos
2
+
3
+ Divergência da casa: o shadcn atual só tem size-4 fixo; mantivemos sm/default/lg pra cobrir botão
4
+ (sm) e estados maiores (lg). O aria-label Carregando já vem embutido.
5
+
6
+ ```tsx preview
7
+ <Spinner size="sm" />
8
+ <Spinner />
9
+ <Spinner size="lg" />
10
+ ```
11
+
12
+ ## No botão
13
+
14
+ Ação em andamento: disabled + Spinner sm + verbo no gerúndio — substitui o Loader2Icon inline
15
+ repetido.
16
+
17
+ ```tsx preview
18
+ <Button disabled><Spinner size="sm" /> Publicando…</Button>
19
+ ```
20
+
21
+ ## Em carga de conteúdo
22
+
23
+ Espera curta sem forma definida. Quando o conteúdo tem forma conhecida (lista, card), prefira
24
+ Skeleton.
25
+
26
+ ```tsx preview
27
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
28
+ <Spinner />
29
+ <span>Carregando sessões…</span>
30
+ </div>
31
+ ```
32
+
33
+ ## Props
34
+
35
+ | Prop | Tipo | Default | Descrição |
36
+ |---|---|---|---|
37
+ | `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | O tamanho — sm pra dentro de botão, lg pra estados de página. |
@@ -0,0 +1,33 @@
1
+ ## Split & Pane
2
+
3
+ `Split` é o primitive espacial da UI. A ordem dos `Pane` define esquerda→direita (ou cima→baixo); o pane não sabe onde está. Sem `resizable`, é flex simples. Com ele, a fronteira vira arrastável sem trocar o modelo do componente.
4
+
5
+ ```tsx preview
6
+ render(
7
+ <div className="h-72 overflow-hidden rounded-lg border border-border">
8
+ <Split resizable>
9
+ <Pane initialSize={30} minSize={20} inset="none" className="bg-muted/30">
10
+ <div className="p-4 text-sm">Navegação</div>
11
+ </Pane>
12
+ <Pane grow inset="lg">
13
+ <p className="text-sm text-muted-foreground">Conteúdo. Arraste a divisória.</p>
14
+ </Pane>
15
+ </Split>
16
+ </div>,
17
+ )
18
+ ```
19
+
20
+ ## Inset
21
+
22
+ O `Pane` traz `inset="md"` por default. Use `none` para chrome, navegação e conteúdo que já possui padding próprio; `sm` e `lg` atendem densidades comuns. `className` sempre pode ajustar o caso específico.
23
+
24
+ ## Rail
25
+
26
+ Um rail não é uma prop especial: é só o último pane.
27
+
28
+ ```tsx
29
+ <Split resizable>
30
+ <Pane grow inset="none"><Main /></Pane>
31
+ <Pane initialSize={28} minSize={20} inset="none"><Inspector /></Pane>
32
+ </Split>
33
+ ```
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: Armazenamento
3
+ ---
4
+
5
+ # Armazenamento
6
+
7
+ > **Experimental.** Superfície mínima, sem consumidor interno ainda — cresce por
8
+ > reincidência de caso real, não por especulação.
9
+
10
+ Upload, download, remoção e URL de acesso — o que backoffice precisa de arquivos
11
+ (anexo de nota, foto de produto, contrato). O contrato é um adapter do core
12
+ (`StorageAdapter`): a key é um **caminho lógico** (`clientes/123/contrato.pdf`) e o
13
+ driver resolve onde ela mora.
14
+
15
+ ## O contrato
16
+
17
+ ```ts
18
+ interface StorageAdapter {
19
+ put(key, data: Uint8Array, opts?: { contentType?: string }): Promise<void>
20
+ get(key): Promise<{ data: Uint8Array; contentType?: string } | null>
21
+ delete(key): Promise<void> // idempotente
22
+ url(key, opts?: { expiresInSeconds?: number }): Promise<string>
23
+ }
24
+ ```
25
+
26
+ Keys passam pela mesma régua em todo driver (`normalizeKey`): relativas, sem `..`,
27
+ sem `\` — traversal é barrado no contrato, não em cada driver.
28
+
29
+ ## Drivers
30
+
31
+ ```ts
32
+ import { fsStorage } from '@softize/opus/storage/fs'
33
+ import { s3Storage } from '@softize/opus/storage/s3'
34
+ import { S3Client } from '@aws-sdk/client-s3'
35
+
36
+ // Dev / single-host: arquivos sob um root local; url() exige baseUrl (o app serve o root).
37
+ const dev = fsStorage({ root: './var/arquivos', baseUrl: 'http://localhost:5173/arquivos' })
38
+
39
+ // Produção: S3 e compatíveis (MinIO, R2…). O client é seu — região/endpoint/credencial.
40
+ const prod = s3Storage({
41
+ client: new S3Client({ region: 'us-east-1' }),
42
+ bucket: 'meu-app',
43
+ prefix: 'uploads',
44
+ })
45
+ ```
46
+
47
+ Os peers do S3 são **opcionais** (`@aws-sdk/client-s3`; `s3-request-presigner` pro
48
+ `url()`): quem não usa o driver não os instala — importados sob demanda.
49
+
50
+ ## No runtime
51
+
52
+ ```ts
53
+ const runtime = createRuntime({
54
+ // …server/data/auth…
55
+ storage: prod,
56
+ })
57
+
58
+ // No handler: ctx.storage (null quando não configurado).
59
+ handler: async (ctx, input) => {
60
+ await ctx.storage!.put(`notas/${input.id}.pdf`, bytes, { contentType: 'application/pdf' })
61
+ return { url: await ctx.storage!.url(`notas/${input.id}.pdf`, { expiresInSeconds: 300 }) }
62
+ }
63
+ ```
64
+
65
+ ## Limites (por enquanto)
66
+
67
+ Bytes em memória (`Uint8Array`), sem streams — arquivo gigante ainda não é o caso;
68
+ sem `list`, sem metadata rica. Cada um entra quando um projeto real cobrar — é a
69
+ régua do Opus: comportamento vira peça na reincidência.