@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,479 @@
1
+ /**
2
+ * <Select /> — O seletor da casa: UM componente pra escolher valor(es) de uma lista.
3
+ *
4
+ * Antes eram QUATRO (NativeSelect, Select-Radix, Combobox, ComposerSelect) com TRÊS
5
+ * motores diferentes — e a escolha entre eles era uma decisão que ninguém acertava
6
+ * (o episódio real: uma tela de filtros saiu com `<select>` cru porque "qual usar?"
7
+ * custava mais que copiar classes). Aqui a decisão virou PROP:
8
+ *
9
+ * <Select native … /> → o <select> do sistema (picker do SO no
10
+ * celular, form nativo, sem portal)
11
+ * <Select … /> → custom (cmdk + Popover)
12
+ * <Select searchable … /> → com busca (o antigo Combobox)
13
+ * <Select multiple … /> → chips removíveis
14
+ * <Select variant="ghost" … /> → discreto, pra barra de composer
15
+ *
16
+ * O TIPO impede o componente de mentir: `native` é uma união discriminada, então
17
+ * `searchable`/`multiple`/`variant` nem compilam junto dele — o menu é do sistema
18
+ * operacional e não aceita busca, chip nem estilo. Erro de compilação > bug silencioso.
19
+ *
20
+ * Régua de escolha (achabilidade, não performance): lista curta e conhecida → `native`;
21
+ * lista longa ou que cresce → `searchable`; item que precisa de JSX → `content`.
22
+ */
23
+ import * as React from 'react'
24
+ import { Check, ChevronDownIcon, X } from 'lucide-react'
25
+ import { Command as CommandPrimitive } from 'cmdk'
26
+
27
+ import { cn } from '../../lib/cn.ts'
28
+ import { CommandEmpty, CommandGroup, CommandItem, CommandList } from './command.tsx'
29
+ import { Popover, PopoverAnchor, PopoverContent } from './popover.tsx'
30
+
31
+ export interface SelectOption {
32
+ value: string
33
+ /** Texto da opção — é o que a BUSCA casa, o que o gatilho mostra e o que o leitor de
34
+ * tela anuncia. Sempre string (JSX rico vai em `content`). */
35
+ label: string
36
+ /** Render rico do item NO MENU (ícone, avatar, duas linhas). Default: o `label`.
37
+ * Só no modo custom — o menu nativo do SO aceita texto puro. */
38
+ content?: React.ReactNode
39
+ /** Detalhe discreto à direita do item (ex.: a chave da task). */
40
+ hint?: string
41
+ /** O que o GATILHO mostra quando esta é a escolhida (default: `label`) — pra quando o
42
+ * nome completo não cabe na barra e só a chave curta serve. */
43
+ triggerLabel?: React.ReactNode
44
+ /** Agrupa as opções sob um rótulo (o `<optgroup>` do nativo, o grupo do menu custom). */
45
+ group?: string
46
+ disabled?: boolean
47
+ }
48
+
49
+ interface SelectBaseProps {
50
+ options: SelectOption[]
51
+ /** Texto do campo vazio. */
52
+ placeholder?: string
53
+ disabled?: boolean
54
+ className?: string
55
+ id?: string
56
+ /** Ícone LEADING dentro do campo — identifica o filtro sem jogar o ícone ao lado. */
57
+ icon?: React.ReactNode
58
+ /** Altura: `default` (h-9, a do Input/Button) ou `sm` (h-8) pra toolbar densa. */
59
+ size?: 'sm' | 'default'
60
+ 'aria-label'?: string
61
+ }
62
+
63
+ /** Só o que o menu do SISTEMA suporta — nada de busca, chip ou variante visual. */
64
+ interface SelectNativeProps extends SelectBaseProps {
65
+ native: true
66
+ value: string
67
+ onChange: (value: string) => void
68
+ searchable?: never
69
+ multiple?: never
70
+ variant?: never
71
+ onSearch?: never
72
+ loading?: never
73
+ clearable?: never
74
+ }
75
+
76
+ interface SelectCustomBase extends SelectBaseProps {
77
+ native?: false
78
+ /** Liga a busca no próprio campo (lista longa/que cresce). */
79
+ searchable?: boolean
80
+ /** Busca SERVER-SIDE (debounced): quando definida, o filtro local desliga e o pai
81
+ * atualiza `options`. Implica `searchable`. */
82
+ onSearch?: (query: string) => void
83
+ /** "Buscando…" enquanto o fetch corre (use com `onSearch`). */
84
+ loading?: boolean
85
+ /** X no campo quando há seleção — limpa num clique. Bom em filtro. */
86
+ clearable?: boolean
87
+ /** `ghost`: sem borda nem fundo, discreto — o seletor da barra do composer. */
88
+ variant?: 'default' | 'ghost'
89
+ /** Placeholder enquanto se digita a busca (cai pro `placeholder`). */
90
+ searchPlaceholder?: string
91
+ emptyText?: string
92
+ /** Ação custom no FIM do campo, dentro do controle (antes do chevron) — ex.: um botão que
93
+ * age sobre o valor escolhido. O clique NÃO abre a lista (o slot para a propagação). */
94
+ trailing?: React.ReactNode
95
+ }
96
+
97
+ export type SelectProps =
98
+ | SelectNativeProps
99
+ | (SelectCustomBase & { multiple?: false; value: string; onChange: (value: string) => void })
100
+ | (SelectCustomBase & { multiple: true; value: string[]; onChange: (value: string[]) => void })
101
+
102
+ /** Ordena preservando a ordem de aparição dos grupos (sem grupo primeiro). */
103
+ function byGroup(options: SelectOption[]): Array<[string | undefined, SelectOption[]]> {
104
+ const out: Array<[string | undefined, SelectOption[]]> = []
105
+ for (const o of options) {
106
+ const found = out.find(([g]) => g === o.group)
107
+ if (found) found[1].push(o)
108
+ else out.push([o.group, [o]])
109
+ }
110
+ return out
111
+ }
112
+
113
+ export function Select(props: SelectProps): React.ReactElement {
114
+ return props.native === true ? <NativeSelect {...props} /> : <CustomSelect {...props} />
115
+ }
116
+
117
+ // ── Nativo: o <select> do sistema ───────────────────────────────────────────────────
118
+ function NativeSelect({
119
+ options,
120
+ placeholder,
121
+ disabled,
122
+ className,
123
+ id,
124
+ icon,
125
+ size = 'default',
126
+ value,
127
+ onChange,
128
+ ...rest
129
+ }: SelectNativeProps): React.ReactElement {
130
+ const groups = byGroup(options)
131
+ return (
132
+ <div className="group/select relative w-full min-w-0 has-[select:disabled]:opacity-50" data-slot="select-wrapper">
133
+ {icon ? (
134
+ <span
135
+ className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground select-none [&_svg]:size-4"
136
+ aria-hidden="true"
137
+ data-slot="select-leading"
138
+ >
139
+ {icon}
140
+ </span>
141
+ ) : null}
142
+ <select
143
+ data-slot="select"
144
+ data-size={size}
145
+ id={id}
146
+ disabled={disabled}
147
+ value={value}
148
+ aria-label={rest['aria-label']}
149
+ onChange={(e) => onChange(e.target.value)}
150
+ className={cn(
151
+ "h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 dark:bg-input/30 dark:hover:bg-input/50",
152
+ 'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
153
+ 'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
154
+ icon ? 'pl-9' : '',
155
+ className,
156
+ )}
157
+ >
158
+ {placeholder !== undefined && (
159
+ <option value="" disabled>
160
+ {placeholder}
161
+ </option>
162
+ )}
163
+ {groups.map(([group, items]) =>
164
+ group === undefined ? (
165
+ items.map((o) => (
166
+ <option key={o.value} value={o.value} disabled={o.disabled} className="bg-[Canvas] text-[CanvasText]">
167
+ {o.label}
168
+ </option>
169
+ ))
170
+ ) : (
171
+ <optgroup key={group} label={group} className="bg-[Canvas] text-[CanvasText]">
172
+ {items.map((o) => (
173
+ <option key={o.value} value={o.value} disabled={o.disabled} className="bg-[Canvas] text-[CanvasText]">
174
+ {o.label}
175
+ </option>
176
+ ))}
177
+ </optgroup>
178
+ ),
179
+ )}
180
+ </select>
181
+ <ChevronDownIcon
182
+ className="pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none"
183
+ aria-hidden="true"
184
+ data-slot="select-icon"
185
+ />
186
+ </div>
187
+ )
188
+ }
189
+
190
+ // ── Custom: cmdk + Popover (busca, multi, item rico, ghost) ─────────────────────────
191
+ function CustomSelect(props: Exclude<SelectProps, SelectNativeProps>): React.ReactElement {
192
+ const {
193
+ options,
194
+ placeholder = 'Selecione…',
195
+ searchPlaceholder,
196
+ emptyText = 'Nada encontrado.',
197
+ disabled,
198
+ className,
199
+ id,
200
+ icon,
201
+ size = 'default',
202
+ searchable = false,
203
+ onSearch,
204
+ loading,
205
+ clearable,
206
+ trailing,
207
+ variant = 'default',
208
+ } = props
209
+ // Busca server-side implica campo buscável (o pai filtra).
210
+ const canSearch = searchable || onSearch !== undefined
211
+ const selectedValues = props.multiple ? props.value : props.value ? [props.value] : []
212
+ const singleValue = props.multiple ? undefined : props.value
213
+
214
+ const [open, setOpen] = React.useState(false)
215
+ const [query, setQuery] = React.useState('')
216
+ const rootRef = React.useRef<HTMLDivElement>(null)
217
+ const inputRef = React.useRef<HTMLInputElement>(null)
218
+
219
+ const selectedOption = options.find((o) => o.value === singleValue)
220
+ // Single: fechado, o campo mostra o rótulo escolhido (sincroniza com value/options).
221
+ React.useEffect(() => {
222
+ if (singleValue === undefined || open) return
223
+ setQuery(selectedOption?.label ?? '')
224
+ }, [singleValue, selectedOption, open])
225
+
226
+ // O cmdk carimba o id DELE no input (a11y interna) e descarta o nosso — reaplicamos por
227
+ // ref, senão o `htmlFor` do Label não acha campo nenhum (clicar no rótulo não focaria).
228
+ React.useEffect(() => {
229
+ if (id !== undefined && inputRef.current !== null) inputRef.current.id = id
230
+ }, [id])
231
+
232
+ const onSearchRef = React.useRef(onSearch)
233
+ onSearchRef.current = onSearch
234
+ React.useEffect(() => {
235
+ if (onSearchRef.current === undefined || !open) return
236
+ const t = setTimeout(() => onSearchRef.current?.(query.trim()), 200)
237
+ return () => clearTimeout(t)
238
+ }, [query, open])
239
+
240
+ const onSelectOption = (o: SelectOption): void => {
241
+ if (o.disabled === true) return
242
+ if (props.multiple) {
243
+ props.onChange(props.value.includes(o.value) ? props.value.filter((v) => v !== o.value) : [...props.value, o.value])
244
+ setQuery('')
245
+ inputRef.current?.focus()
246
+ } else {
247
+ props.onChange(o.value)
248
+ setQuery(o.label)
249
+ setOpen(false)
250
+ inputRef.current?.blur()
251
+ }
252
+ }
253
+ const removeValue = (v: string): void => {
254
+ if (props.multiple) props.onChange(props.value.filter((x) => x !== v))
255
+ }
256
+
257
+ const allSelected = options.length > 0 && options.every((o) => selectedValues.includes(o.value))
258
+ const showToggleAll = props.multiple === true && options.length > 1 && onSearch === undefined && query.trim() === ''
259
+ const toggleAll = (): void => {
260
+ if (!props.multiple) return
261
+ props.onChange(allSelected ? [] : options.map((o) => o.value))
262
+ inputRef.current?.focus()
263
+ }
264
+
265
+ const ghost = variant === 'ghost'
266
+ // Não-buscável: o campo é só gatilho — readOnly (sem caret, sem digitar), o clique abre.
267
+ // Um render só pros dois modos; o que muda é poder digitar.
268
+ const triggerText = !props.multiple && !open && selectedOption?.triggerLabel !== undefined ? undefined : query
269
+
270
+ return (
271
+ <Popover open={open} onOpenChange={setOpen}>
272
+ <div ref={rootRef} data-slot="select" className={cn(ghost ? 'min-w-0' : 'min-w-40', className)}>
273
+ <CommandPrimitive data-slot="select-command" shouldFilter={canSearch && onSearch === undefined} className="overflow-visible">
274
+ <PopoverAnchor asChild>
275
+ <div
276
+ data-slot="select-control"
277
+ data-size={size}
278
+ data-variant={variant}
279
+ onClick={() => {
280
+ if (disabled !== true) {
281
+ setOpen(true)
282
+ inputRef.current?.focus()
283
+ }
284
+ }}
285
+ className={cn(
286
+ 'flex items-center gap-1.5 rounded-md outline-none transition-[color,box-shadow]',
287
+ ghost
288
+ ? 'min-h-7 px-1.5 py-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-within:ring-2 focus-within:ring-ring/50'
289
+ : "min-h-9 flex-wrap border border-input bg-transparent px-3 py-1 focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[size=sm]:min-h-8 data-[size=sm]:py-0.5 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
290
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
291
+ canSearch ? '' : 'cursor-pointer',
292
+ disabled === true && 'cursor-not-allowed opacity-50',
293
+ )}
294
+ >
295
+ {icon ? (
296
+ <span data-slot="select-leading" className="text-muted-foreground">
297
+ {icon}
298
+ </span>
299
+ ) : null}
300
+ {props.multiple &&
301
+ selectedValues.map((v) => {
302
+ const o = options.find((x) => x.value === v)
303
+ return (
304
+ <span key={v} data-slot="select-chip" className="inline-flex items-center gap-1 rounded bg-secondary px-1.5 py-0.5 text-xs text-secondary-foreground">
305
+ {o?.label ?? v}
306
+ <button
307
+ type="button"
308
+ data-slot="select-chip-remove"
309
+ onClick={(e) => {
310
+ e.stopPropagation()
311
+ removeValue(v)
312
+ }}
313
+ className="text-muted-foreground hover:text-foreground"
314
+ aria-label={`Remover ${o?.label ?? v}`}
315
+ >
316
+ <X className="size-3" />
317
+ </button>
318
+ </span>
319
+ )
320
+ })}
321
+ {/* triggerLabel: o gatilho mostra o rótulo CURTO da opção (o input fica
322
+ escondido atrás dele até focar) — o caso da barra do composer. */}
323
+ {triggerText === undefined && (
324
+ <span data-slot="select-trigger-label" className="min-w-0 truncate text-sm">
325
+ {selectedOption?.triggerLabel}
326
+ </span>
327
+ )}
328
+ <CommandPrimitive.Input
329
+ ref={inputRef}
330
+ data-slot="select-input"
331
+ id={id}
332
+ disabled={disabled}
333
+ readOnly={!canSearch}
334
+ aria-label={props['aria-label']}
335
+ value={triggerText ?? ''}
336
+ onValueChange={(v) => {
337
+ if (!canSearch) return
338
+ setQuery(v)
339
+ setOpen(true)
340
+ }}
341
+ onFocus={() => {
342
+ setOpen(true)
343
+ if (!props.multiple && canSearch) inputRef.current?.select()
344
+ }}
345
+ onKeyDown={(e) => {
346
+ if (e.key === 'Escape' && open) {
347
+ e.stopPropagation()
348
+ setOpen(false)
349
+ }
350
+ }}
351
+ placeholder={props.multiple && selectedValues.length > 0 ? '' : (canSearch ? (searchPlaceholder ?? placeholder) : placeholder)}
352
+ className={cn(
353
+ 'min-w-0 flex-1 bg-transparent text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed',
354
+ canSearch ? '' : 'cursor-pointer caret-transparent',
355
+ triggerText === undefined && 'sr-only',
356
+ )}
357
+ />
358
+ {clearable === true && selectedValues.length > 0 && disabled !== true && (
359
+ <button
360
+ type="button"
361
+ data-slot="select-clear"
362
+ aria-label="Limpar seleção"
363
+ onPointerDown={(e) => e.preventDefault()}
364
+ onClick={(e) => {
365
+ e.stopPropagation()
366
+ if (props.multiple) props.onChange([])
367
+ else {
368
+ props.onChange('')
369
+ setQuery('')
370
+ }
371
+ }}
372
+ className="flex shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
373
+ >
374
+ <X className="size-3.5" />
375
+ </button>
376
+ )}
377
+ {/* Ação custom (ex.: botão que age sobre o valor) — o clique NÃO abre a lista:
378
+ o slot para a propagação pro controle. */}
379
+ {trailing !== undefined && (
380
+ <span
381
+ data-slot="select-trailing"
382
+ // Ação no fim do campo é SECUNDÁRIA: o tom muted mora no slot (o botão
383
+ // ghost herda), pra não sprinklar `text-muted-foreground` em cada call
384
+ // site. O hover acende via o próprio `hover:text-accent-foreground` do ghost.
385
+ // Botão trailing na BORDA (buscável, sem chevron): puxa ~6px pra o inset
386
+ // direito casar com o vertical (botão quadrado no canto), como o InputGroup.
387
+ // Com chevron, NÃO puxa — senão cola no chevron (o `gap-1.5` do control).
388
+ className={cn('flex shrink-0 items-center text-muted-foreground', canSearch && 'has-[button]:-mr-1.5')}
389
+ onClick={(e) => e.stopPropagation()}
390
+ onPointerDown={(e) => e.stopPropagation()}
391
+ >
392
+ {trailing}
393
+ </span>
394
+ )}
395
+ {/* Chevron: a affordance de "abre uma lista". Sempre no não-buscável; no
396
+ buscável some (ali quem manda o sinal é o cursor de texto). */}
397
+ {!canSearch && (
398
+ <ChevronDownIcon data-slot="select-icon" className={cn('size-4 shrink-0 opacity-50', ghost ? 'h-3.5 w-3.5 opacity-60' : '')} aria-hidden="true" />
399
+ )}
400
+ </div>
401
+ </PopoverAnchor>
402
+ <PopoverContent
403
+ align="start"
404
+ // No campo de form a lista acompanha a largura do controle; no ghost, NÃO — o
405
+ // gatilho é do tamanho do rótulo (uma chave curta), e a lista precisa caber o
406
+ // título inteiro.
407
+ className={cn(
408
+ 'pointer-events-auto overflow-hidden p-0',
409
+ ghost ? 'w-auto min-w-56 max-w-80' : 'w-(--radix-popover-trigger-width) min-w-40',
410
+ )}
411
+ onOpenAutoFocus={(e) => e.preventDefault()}
412
+ onCloseAutoFocus={(e) => e.preventDefault()}
413
+ onInteractOutside={(e) => {
414
+ const target = e.target as HTMLElement
415
+ if (rootRef.current?.contains(target)) return e.preventDefault()
416
+ const label = target.closest('label')
417
+ if (id !== undefined && label !== null && label.htmlFor === id) e.preventDefault()
418
+ }}
419
+ onWheel={(e) => e.stopPropagation()}
420
+ onTouchMove={(e) => e.stopPropagation()}
421
+ >
422
+ {showToggleAll && (
423
+ <div className="border-b p-1">
424
+ <button
425
+ type="button"
426
+ data-slot="select-toggle-all"
427
+ onClick={toggleAll}
428
+ className="flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
429
+ >
430
+ {allSelected ? 'Limpar seleção' : `Selecionar tudo (${options.length})`}
431
+ </button>
432
+ </div>
433
+ )}
434
+ <CommandList data-slot="select-list" className="max-h-60 overflow-auto">
435
+ {loading === true ? (
436
+ <div data-slot="select-loading" className="py-6 text-center text-sm text-muted-foreground">
437
+ Buscando…
438
+ </div>
439
+ ) : (
440
+ <>
441
+ <CommandEmpty>{emptyText}</CommandEmpty>
442
+ {byGroup(options).map(([group, items]) => (
443
+ <CommandGroup key={group ?? '—'} heading={group}>
444
+ {items.map((o) => {
445
+ const hasHint = o.hint !== undefined && o.hint.length > 0
446
+ return (
447
+ <CommandItem
448
+ key={o.value}
449
+ data-slot="select-item"
450
+ disabled={o.disabled}
451
+ value={`${o.label} ${o.hint ?? ''} ${o.value}`}
452
+ onSelect={() => onSelectOption(o)}
453
+ >
454
+ <span data-slot="select-item-label" className="min-w-0 truncate">
455
+ {o.content ?? o.label}
456
+ </span>
457
+ {hasHint && (
458
+ <span data-slot="select-item-hint" className="ml-auto shrink-0 text-xs text-muted-foreground">
459
+ {o.hint}
460
+ </span>
461
+ )}
462
+ <Check
463
+ data-slot="select-item-check"
464
+ className={cn('size-4 shrink-0', !hasHint && 'ml-auto', selectedValues.includes(o.value) ? 'opacity-100' : 'opacity-0')}
465
+ />
466
+ </CommandItem>
467
+ )
468
+ })}
469
+ </CommandGroup>
470
+ ))}
471
+ </>
472
+ )}
473
+ </CommandList>
474
+ </PopoverContent>
475
+ </CommandPrimitive>
476
+ </div>
477
+ </Popover>
478
+ )
479
+ }
@@ -0,0 +1,26 @@
1
+ import * as React from "react"
2
+ import { Separator as SeparatorPrimitive } from "radix-ui"
3
+
4
+ import { cn } from '../../lib/cn.ts'
5
+
6
+ function Separator({
7
+ className,
8
+ orientation = "horizontal",
9
+ decorative = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12
+ return (
13
+ <SeparatorPrimitive.Root
14
+ data-slot="separator"
15
+ decorative={decorative}
16
+ orientation={orientation}
17
+ className={cn(
18
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ export { Separator }
@@ -0,0 +1,13 @@
1
+ import { cn } from '../../lib/cn.ts'
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("animate-pulse rounded-md bg-accent", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,61 @@
1
+ import * as React from "react"
2
+ import { Slider as SliderPrimitive } from "radix-ui"
3
+
4
+ import { cn } from '../../lib/cn.ts'
5
+
6
+ function Slider({
7
+ className,
8
+ defaultValue,
9
+ value,
10
+ min = 0,
11
+ max = 100,
12
+ ...props
13
+ }: React.ComponentProps<typeof SliderPrimitive.Root>) {
14
+ const _values = React.useMemo(
15
+ () =>
16
+ Array.isArray(value)
17
+ ? value
18
+ : Array.isArray(defaultValue)
19
+ ? defaultValue
20
+ : [min, max],
21
+ [value, defaultValue, min, max]
22
+ )
23
+
24
+ return (
25
+ <SliderPrimitive.Root
26
+ data-slot="slider"
27
+ defaultValue={defaultValue}
28
+ value={value}
29
+ min={min}
30
+ max={max}
31
+ className={cn(
32
+ "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
33
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ <SliderPrimitive.Track
38
+ data-slot="slider-track"
39
+ className={cn(
40
+ "relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
41
+ )}
42
+ >
43
+ <SliderPrimitive.Range
44
+ data-slot="slider-range"
45
+ className={cn(
46
+ "absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
47
+ )}
48
+ />
49
+ </SliderPrimitive.Track>
50
+ {Array.from({ length: _values.length }, (_, index) => (
51
+ <SliderPrimitive.Thumb
52
+ data-slot="slider-thumb"
53
+ key={index}
54
+ className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
55
+ />
56
+ ))}
57
+ </SliderPrimitive.Root>
58
+ )
59
+ }
60
+
61
+ export { Slider }
@@ -0,0 +1,46 @@
1
+ import * as React from 'react'
2
+ import {
3
+ CircleCheckIcon,
4
+ InfoIcon,
5
+ Loader2Icon,
6
+ OctagonXIcon,
7
+ TriangleAlertIcon,
8
+ } from 'lucide-react'
9
+ import { Toaster as Sonner, toast, type ToasterProps } from 'sonner'
10
+
11
+ /**
12
+ * Toaster temático (sonner) — monte 1x no root do app. Os toasts saem estilizados
13
+ * com os tokens do Opus via CSS vars (popover/border/radius) + ícones lucide por
14
+ * tipo. Dispare com `toast.success/error(...)`.
15
+ *
16
+ * Divergência da casa: o shadcn lê o tema com `next-themes` (useTheme); aqui não
17
+ * usamos next-themes — passe `theme` por prop se precisar forçar (default 'system').
18
+ * Elevação dark preservada: `--normal-bg` aponta pro popover (não pro background).
19
+ */
20
+ export function Toaster({ ...props }: ToasterProps): React.ReactElement {
21
+ return (
22
+ <Sonner
23
+ data-slot="toaster"
24
+ theme="system"
25
+ className="toaster group"
26
+ icons={{
27
+ success: <CircleCheckIcon className="size-4" />,
28
+ info: <InfoIcon className="size-4" />,
29
+ warning: <TriangleAlertIcon className="size-4" />,
30
+ error: <OctagonXIcon className="size-4" />,
31
+ loading: <Loader2Icon className="size-4 animate-spin" />,
32
+ }}
33
+ style={
34
+ {
35
+ '--normal-bg': 'var(--popover)',
36
+ '--normal-text': 'var(--popover-foreground)',
37
+ '--normal-border': 'var(--border)',
38
+ '--border-radius': 'var(--radius)',
39
+ } as React.CSSProperties
40
+ }
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { toast }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react'
2
+ import { Loader2Icon } from 'lucide-react'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { cn } from '../../lib/cn.ts'
5
+
6
+ /** Tamanhos do spinner — divergência da casa (o shadcn atual só tem `size-4` fixo).
7
+ * Mantido aditivo pra cobrir botão (sm) e estados maiores (lg). */
8
+ const spinnerVariants = cva('animate-spin', {
9
+ variants: {
10
+ size: { sm: 'size-3', default: 'size-4', lg: 'size-6' },
11
+ },
12
+ defaultVariants: { size: 'default' },
13
+ })
14
+
15
+ export interface SpinnerProps extends React.ComponentProps<'svg'>, VariantProps<typeof spinnerVariants> {}
16
+
17
+ /** Indicador de carregamento (Loader2Icon girando) — substitui o `<Loader2Icon className="animate-spin"/>`
18
+ * inline repetido. Tamanhos sm/default/lg. Pra placeholder de conteúdo, use Skeleton. */
19
+ export function Spinner({ className, size, ...props }: SpinnerProps): React.ReactElement {
20
+ return (
21
+ <Loader2Icon
22
+ data-slot="spinner"
23
+ role="status"
24
+ aria-label="Carregando"
25
+ className={cn(spinnerVariants({ size }), className)}
26
+ {...props}
27
+ />
28
+ )
29
+ }