@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,226 @@
1
+ /**
2
+ * `dialog` — o trio imperativo (alert · confirm · prompt) em UMA linha.
3
+ *
4
+ * await dialog.alert({ title: 'Sessão expirada', description: 'Você será desconectado.' })
5
+ * if (await dialog.confirm({ title: 'Excluir a sessão?', variant: 'destructive' })) …
6
+ * const nome = await dialog.prompt({ title: 'Nome do snapshot' })
7
+ *
8
+ * Mesma forma do `toast` (superfície imperativa + host no shell), com uma diferença: eles
9
+ * RESPONDEM — `alert` devolve `Promise<void>`, `confirm` `Promise<boolean>`, `prompt`
10
+ * `Promise<string | null>`. É o que tira as ~15 linhas de JSX que cada tela montava.
11
+ *
12
+ * Namespace `dialog` de propósito: `window.alert/confirm/prompt` são globais do browser, e
13
+ * um import esquecido cairia no nativo (cinza, sem tema) sem erro. `window.dialog` não
14
+ * existe — a família fica imune ao footgun. `confirm()` segue exportado como alias.
15
+ *
16
+ * Por baixo é o <AlertDialog>: `role="alertdialog"`, não fecha no clique fora, foco preso
17
+ * — a semântica que uma interrupção que EXIGE resposta pede. ESC/Cancelar resolve o "não"
18
+ * de cada tipo (void, `false`, `null`).
19
+ */
20
+
21
+ import * as React from 'react'
22
+ import {
23
+ AlertDialog,
24
+ AlertDialogAction,
25
+ AlertDialogCancel,
26
+ AlertDialogContent,
27
+ AlertDialogDescription,
28
+ AlertDialogFooter,
29
+ AlertDialogHeader,
30
+ AlertDialogMedia,
31
+ AlertDialogTitle,
32
+ } from '../primitives/alert-dialog.tsx'
33
+ import { Input } from '../primitives/input.tsx'
34
+
35
+ /** Campos comuns aos três — o corpo (`body`) é o que o texto da `description` não expressa. */
36
+ interface DialogBase {
37
+ title: React.ReactNode
38
+ description?: React.ReactNode
39
+ /** Ícone grande acima do título (o quadro do AlertDialogMedia). */
40
+ media?: React.ReactNode
41
+ /** Conteúdo próprio entre a descrição e os botões (lista, detalhe destacado). */
42
+ body?: React.ReactNode
43
+ /** Rótulo do botão que confirma. */
44
+ action?: string
45
+ }
46
+
47
+ /** `alert` — reconhecimento obrigatório, um botão só. */
48
+ export interface AlertOptions extends DialogBase {}
49
+
50
+ /** `confirm` — pergunta de sim/não. */
51
+ export interface ConfirmOptions extends DialogBase {
52
+ cancel?: string
53
+ /** `destructive` pinta o botão de confirmar — o que apaga tem que parecer o que apaga. */
54
+ variant?: 'default' | 'destructive'
55
+ }
56
+
57
+ /** `prompt` — uma string livre. Form de verdade (campos, contrato) → ActionFormDialog. */
58
+ export interface PromptOptions extends DialogBase {
59
+ cancel?: string
60
+ variant?: 'default' | 'destructive'
61
+ placeholder?: string
62
+ defaultValue?: string
63
+ }
64
+
65
+ type Pending =
66
+ | ({ kind: 'alert'; id: number; resolve: () => void } & AlertOptions)
67
+ | ({ kind: 'confirm'; id: number; resolve: (ok: boolean) => void } & ConfirmOptions)
68
+ | ({ kind: 'prompt'; id: number; resolve: (value: string | null) => void } & PromptOptions)
69
+
70
+ let nextId = 0
71
+ let listener: ((p: Pending) => void) | null = null
72
+
73
+ /** Sem `<DialogHost />` montado, LANÇA em vez de deixar a promise pendurada (clique sem
74
+ * efeito é o pior desfecho de uma interrupção que exige resposta). */
75
+ function requireHost(fn: string): Error | null {
76
+ if (listener !== null) return null
77
+ return new Error(
78
+ `${fn} foi chamado sem <DialogHost /> montado — ponha o host no shell do app (ao lado do <Toaster />).`,
79
+ )
80
+ }
81
+
82
+ export const dialog = {
83
+ /** Avisa e espera o "OK". `Promise<void>` — só reconhecimento, sem decisão. */
84
+ alert(options: AlertOptions): Promise<void> {
85
+ const err = requireHost('dialog.alert()')
86
+ if (err !== null) return Promise.reject(err)
87
+ return new Promise<void>((resolve) => {
88
+ listener?.({ ...options, kind: 'alert', id: nextId++, resolve })
89
+ })
90
+ },
91
+ /** Pergunta sim/não. `Promise<boolean>` — fechar/ESC resolve `false`. */
92
+ confirm(options: ConfirmOptions): Promise<boolean> {
93
+ const err = requireHost('dialog.confirm()')
94
+ if (err !== null) return Promise.reject(err)
95
+ return new Promise<boolean>((resolve) => {
96
+ listener?.({ ...options, kind: 'confirm', id: nextId++, resolve })
97
+ })
98
+ },
99
+ /** Pede uma string. `Promise<string | null>` — fechar/ESC/Cancelar resolve `null`. */
100
+ prompt(options: PromptOptions): Promise<string | null> {
101
+ const err = requireHost('dialog.prompt()')
102
+ if (err !== null) return Promise.reject(err)
103
+ return new Promise<string | null>((resolve) => {
104
+ listener?.({ ...options, kind: 'prompt', id: nextId++, resolve })
105
+ })
106
+ },
107
+ }
108
+
109
+ /** Alias retrocompatível — `confirm()` é `dialog.confirm()`. */
110
+ export function confirm(options: ConfirmOptions): Promise<boolean> {
111
+ return dialog.confirm(options)
112
+ }
113
+
114
+ /**
115
+ * O host — um por app, no shell. Enfileira: uma por vez (duas caixas empilhadas seriam
116
+ * ambíguas sobre qual clique respondeu o quê).
117
+ */
118
+ export function DialogHost(): React.ReactElement {
119
+ const [queue, setQueue] = React.useState<Pending[]>([])
120
+ const current = queue[0]
121
+ const [text, setText] = React.useState('')
122
+
123
+ React.useEffect(() => {
124
+ listener = (p) => setQueue((q) => [...q, p])
125
+ return () => {
126
+ listener = null
127
+ }
128
+ }, [])
129
+
130
+ // Reseta o input quando a pergunta corrente troca (defaultValue do prompt, ou vazio).
131
+ React.useEffect(() => {
132
+ setText(current !== undefined && current.kind === 'prompt' ? (current.defaultValue ?? '') : '')
133
+ }, [current?.id]) // eslint-disable-line react-hooks/exhaustive-deps
134
+
135
+ // Resolve a pergunta `id` — e SÓ ela. A 2ª chamada pro mesmo id (clique + o fechamento
136
+ // que o Radix dispara em seguida) é inócua: a promise já resolveu e a fila já andou.
137
+ const drop = (id: number, apply: (p: Pending) => void): void => {
138
+ setQueue((q) => {
139
+ if (q[0]?.id !== id) return q
140
+ apply(q[0])
141
+ return q.slice(1)
142
+ })
143
+ }
144
+ // Fechar É responder "não" — o "não" de cada tipo.
145
+ const cancel = (p: Pending): void => {
146
+ if (p.kind === 'alert') p.resolve()
147
+ else if (p.kind === 'confirm') p.resolve(false)
148
+ else p.resolve(null)
149
+ }
150
+ const accept = (p: Pending, value: string): void => {
151
+ if (p.kind === 'alert') p.resolve()
152
+ else if (p.kind === 'confirm') p.resolve(true)
153
+ else p.resolve(value)
154
+ }
155
+
156
+ const variant = current !== undefined && current.kind !== 'alert' ? current.variant : undefined
157
+ const defaultAction = current?.kind === 'alert' ? 'OK' : 'Confirmar'
158
+
159
+ return (
160
+ <AlertDialog
161
+ open={current !== undefined}
162
+ onOpenChange={(open) => {
163
+ if (!open && current !== undefined) drop(current.id, cancel)
164
+ }}
165
+ >
166
+ {current !== undefined && (
167
+ <AlertDialogContent
168
+ data-slot="dialog"
169
+ data-kind={current.kind}
170
+ // Prompt: o foco vai pro input, não pro Cancelar (default do AlertDialog). Via
171
+ // querySelector (não ref) — o Input é function component sem forwardRef, e isto
172
+ // funciona igual em React 18 e 19.
173
+ onOpenAutoFocus={
174
+ current.kind === 'prompt'
175
+ ? (e) => {
176
+ e.preventDefault()
177
+ const root = (e.currentTarget as HTMLElement | null) ?? document
178
+ const el = root.querySelector<HTMLInputElement>('[data-slot="input"]')
179
+ el?.focus()
180
+ el?.select()
181
+ }
182
+ : undefined
183
+ }
184
+ >
185
+ <AlertDialogHeader>
186
+ {current.media !== undefined && <AlertDialogMedia>{current.media}</AlertDialogMedia>}
187
+ <AlertDialogTitle>{current.title}</AlertDialogTitle>
188
+ {current.description !== undefined && (
189
+ <AlertDialogDescription>{current.description}</AlertDialogDescription>
190
+ )}
191
+ </AlertDialogHeader>
192
+ {current.body !== undefined && <div data-slot="dialog-body">{current.body}</div>}
193
+ {current.kind === 'prompt' && (
194
+ <Input
195
+ value={text}
196
+ placeholder={current.placeholder}
197
+ onChange={(e) => setText(e.target.value)}
198
+ onKeyDown={(e) => {
199
+ if (e.key === 'Enter') {
200
+ e.preventDefault()
201
+ drop(current.id, (p) => accept(p, text))
202
+ }
203
+ }}
204
+ />
205
+ )}
206
+ <AlertDialogFooter>
207
+ {current.kind !== 'alert' && (
208
+ <AlertDialogCancel onClick={() => drop(current.id, cancel)}>
209
+ {current.cancel ?? 'Cancelar'}
210
+ </AlertDialogCancel>
211
+ )}
212
+ <AlertDialogAction
213
+ variant={variant === 'destructive' ? 'destructive' : 'default'}
214
+ onClick={() => drop(current.id, (p) => accept(p, text))}
215
+ >
216
+ {current.action ?? defaultAction}
217
+ </AlertDialogAction>
218
+ </AlertDialogFooter>
219
+ </AlertDialogContent>
220
+ )}
221
+ </AlertDialog>
222
+ )
223
+ }
224
+
225
+ /** Alias retrocompatível — `<ConfirmHost />` é `<DialogHost />`. */
226
+ export const ConfirmHost = DialogHost
@@ -0,0 +1,75 @@
1
+ /**
2
+ * <DataState /> — o estado "carregando" da casa: o que mostrar ANTES do conteúdo chegar.
3
+ *
4
+ * Orquestra os estados de uma carga assíncrona (lista/fetch) num lugar só: erro (calmo, sem
5
+ * jargão na tela), carregando (Spinner centralizado), vazio (texto) e, por fim, o conteúdo.
6
+ * Dissolve o `error ? … : isLoading ? <Spinner/> : empty ? … : children` repetido em toda
7
+ * tela. NÃO confundir com "processando" (AÇÃO em andamento depois do clique) — isso é o
8
+ * `busy` do Button. Pra placeholder com forma de conteúdo, use Skeleton.
9
+ */
10
+ import * as React from 'react'
11
+ import { cn } from '../../lib/cn.ts'
12
+ import { Spinner } from '../primitives/spinner.tsx'
13
+
14
+ export interface DataStateProps {
15
+ /** Carregando (antes do conteúdo) — mostra o Spinner centralizado. */
16
+ loading?: boolean
17
+ /** Erro da carga. A mensagem técnica NÃO vai pra tela (passe `errorText` pra um aviso). */
18
+ error?: { message?: string } | null
19
+ /** Sem itens — mostra o `emptyText`. */
20
+ empty?: boolean
21
+ /** Texto do vazio (pt-BR, ex.: "Nenhum papel."). */
22
+ emptyText?: string
23
+ /** Aviso de erro (calmo, orientado ao usuário). Default: "Não foi possível carregar.". */
24
+ errorText?: string
25
+ /** Em TABELA: renderiza o estado como UMA linha (<tr><td colSpan>), não um bloco — pra
26
+ * caber direto no <tbody>. Passe o nº de colunas da tabela. */
27
+ colSpan?: number
28
+ /** O conteúdo — renderizado só quando não há erro/carregando/vazio. */
29
+ children: React.ReactNode
30
+ }
31
+
32
+ /** Ordem: erro → carregando → vazio → conteúdo (espelha o ternário que substitui). */
33
+ export function DataState({
34
+ loading = false,
35
+ error = null,
36
+ empty = false,
37
+ emptyText = 'Nada por aqui.',
38
+ errorText,
39
+ colSpan,
40
+ children,
41
+ }: DataStateProps): React.ReactElement {
42
+ // Há conteúdo: passa direto (no modo tabela, são as <tr> dos itens).
43
+ if (!error && !loading && !empty) return <>{children}</>
44
+
45
+ // Modo TABELA: o estado vira UMA linha de largura cheia (<td colSpan>), pra caber no <tbody>.
46
+ if (colSpan !== undefined) {
47
+ return (
48
+ <tr>
49
+ <td
50
+ colSpan={colSpan}
51
+ className={cn('px-4 py-10 text-center text-sm', error ? 'text-destructive' : 'text-muted-foreground/60')}
52
+ >
53
+ {error ? (errorText ?? 'Não foi possível carregar.') : loading ? <Spinner className="mx-auto" /> : emptyText}
54
+ </td>
55
+ </tr>
56
+ )
57
+ }
58
+
59
+ // Modo BLOCO (default).
60
+ if (error) {
61
+ return (
62
+ <div className="rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3 text-center text-sm text-destructive">
63
+ {errorText ?? 'Não foi possível carregar.'}
64
+ </div>
65
+ )
66
+ }
67
+ if (loading) {
68
+ return (
69
+ <div className="py-10 text-center text-muted-foreground">
70
+ <Spinner className="mx-auto" />
71
+ </div>
72
+ )
73
+ }
74
+ return <p className="py-10 text-center text-sm text-muted-foreground/60">{emptyText}</p>
75
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * <ActionFormDialog /> — o ActionForm dentro de um Dialog do Opus (o "modo modal").
3
+ *
4
+ * Wrapper fino: Dialog + DialogHeader/Title em volta do <ActionForm>. Fecha o dialog
5
+ * no sucesso (onOpenChange(false)) e o Cancelar fecha também. O ActionForm continua
6
+ * dono do <form>/submit (o botão de submit precisa estar DENTRO do form), por isso o
7
+ * corpo e o rodapé entram como SLOTS — DialogBody/DialogFooter injetados, renderizados
8
+ * pelo próprio form (fonte única do scroll e da faixa, sem string duplicada).
9
+ */
10
+
11
+ import {
12
+ Dialog,
13
+ DialogBody,
14
+ DialogContent,
15
+ DialogFooter,
16
+ DialogHeader,
17
+ DialogTitle,
18
+ DialogDescription,
19
+ } from '../primitives/dialog.tsx'
20
+ import { ActionForm, type ActionFormProps } from './form.tsx'
21
+
22
+ export interface ActionFormDialogProps<TInput extends Record<string, unknown>, TData>
23
+ extends ActionFormProps<TInput, TData> {
24
+ open: boolean
25
+ onOpenChange: (open: boolean) => void
26
+ title: string
27
+ description?: string
28
+ }
29
+
30
+ export function ActionFormDialog<TInput extends Record<string, unknown>, TData>({
31
+ open,
32
+ onOpenChange,
33
+ title,
34
+ description,
35
+ onSuccess,
36
+ onCancel,
37
+ ...rest
38
+ }: ActionFormDialogProps<TInput, TData>) {
39
+ return (
40
+ <Dialog open={open} onOpenChange={onOpenChange}>
41
+ {/* Header e rodapé FIXOS, só o CORPO (campos) scrolla — o "X" não rola junto e
42
+ forms longos não estouram a viewport. O content já é coluna flex com teto. */}
43
+ <DialogContent>
44
+ <DialogHeader>
45
+ <DialogTitle>{title}</DialogTitle>
46
+ {description !== undefined && <DialogDescription>{description}</DialogDescription>}
47
+ </DialogHeader>
48
+ <ActionForm
49
+ {...rest}
50
+ className="min-h-0 flex-1"
51
+ // Slots injetados: o corpo vira DialogBody (rola), o rodapé vira DialogFooter (a
52
+ // faixa da casa — fonte única). Os botões seguem dentro do <form> do ActionForm.
53
+ body={(fields) => <DialogBody>{fields}</DialogBody>}
54
+ footer={(actions) => <DialogFooter>{actions}</DialogFooter>}
55
+ onSuccess={(d) => {
56
+ onSuccess?.(d)
57
+ onOpenChange(false)
58
+ }}
59
+ onCancel={onCancel ?? (() => onOpenChange(false))}
60
+ />
61
+ </DialogContent>
62
+ </Dialog>
63
+ )
64
+ }