@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,196 @@
1
+ /**
2
+ * <ActionTrigger action input /> — botão que dispara uma SimpleAction (sem form).
3
+ *
4
+ * Use pra mutações sem form: assign, escalate, close, archive, excluir.
5
+ * - Loading state automático (disabled + "..." enquanto roda).
6
+ * - Toast em sucesso/erro; cache invalidation via action.invalidates.
7
+ * - Confirmação opcional via <AlertDialog> (compacto, não fecha no clique fora) antes de disparar.
8
+ * - `icon` faz o botão virar icon-only com tooltip: é a ação que mora NO item (linha,
9
+ * card). Absorveu o antigo DeleteButton, que era este componente com uma lixeira.
10
+ * Emite `data-action="<action.name>"` na raiz (selector E2E).
11
+ */
12
+
13
+ import type { ReactNode } from 'react'
14
+ import { useState } from 'react'
15
+ import type { SimpleContract } from '../../../core/index.ts'
16
+ import { useTriggerAction } from '../../drivers/react.tsx'
17
+ import { toast } from '../primitives/sonner.tsx'
18
+ import { cn } from '../../lib/cn.ts'
19
+ import { Button } from '../primitives/button.tsx'
20
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip.tsx'
21
+ import {
22
+ AlertDialog,
23
+ AlertDialogContent,
24
+ AlertDialogDescription,
25
+ AlertDialogFooter,
26
+ AlertDialogHeader,
27
+ AlertDialogTitle,
28
+ } from '../primitives/alert-dialog.tsx'
29
+
30
+ /** action.messages.* pode ser string ou I18nRef ({ key, default }). Resolve pra texto. */
31
+ function msgText(m: unknown, fallback: string): string {
32
+ if (typeof m === 'string') return m
33
+ if (m !== null && typeof m === 'object' && 'default' in m) {
34
+ const d = (m as { default: unknown }).default
35
+ if (typeof d === 'string') return d
36
+ }
37
+ return fallback
38
+ }
39
+
40
+ export interface ActionTriggerProps<TInput, TData> {
41
+ action: SimpleContract<TInput, TData>
42
+ /** Input enviado pra action. Geralmente { id } ou similar. */
43
+ input: TInput
44
+ /** Texto do botão. Default usa action.label. */
45
+ label?: string
46
+ /** Variante visual do Button. Default: 'default' (ou 'destructive' se o
47
+ * action.confirm declarar destructive). */
48
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
49
+ /** Tamanho do botão. */
50
+ size?: 'default' | 'sm' | 'lg' | 'icon'
51
+ /** Desabilita o botão independente de loading. */
52
+ disabled?: boolean
53
+ /** Texto do confirm dialog. Default vem do `action.confirm` (ConfirmSpec do
54
+ * contrato — declarativo). Sem nenhum dos dois, dispara direto. */
55
+ confirm?: {
56
+ title: string
57
+ description?: string
58
+ actionLabel?: string
59
+ cancelLabel?: string
60
+ }
61
+ /** Callback adicional pós-sucesso (cache já foi invalidado). */
62
+ onSuccess?: (data: TData) => void
63
+ /** Ícone: o botão vira icon-only, com o `label` no tooltip e no aria-label. É a forma
64
+ * da ação que mora DENTRO de um item (linha, card) — o clique não vaza pro item. */
65
+ icon?: ReactNode
66
+ /** Nome do item na pergunta (ex.: o slug, o nome da unidade) — sai entre aspas, em
67
+ * destaque, antes da mensagem do contrato. */
68
+ itemLabel?: string
69
+ className?: string
70
+ }
71
+
72
+ /**
73
+ * Categorias de erro cuja frase do SERVIDOR vence o rótulo do contrato: "Este agente tem 3
74
+ * conversas — desabilite em vez de excluir" é acionável; "Falha ao excluir" não. É
75
+ * ALLOWLIST de propósito — erro inesperado carrega texto técnico cru (uma violação de FK
76
+ * viraria `violates foreign key constraint "user_unit_id_fkey"` no toast de quem só clicou
77
+ * no botão), então categoria desconhecida cai no rótulo genérico, o lado seguro de errar.
78
+ * `authorization` fica de fora: o runtime lança `Forbidden`, pior que a frase em pt-BR.
79
+ */
80
+ const BUSINESS_ERRORS = ['conflict', 'validation', 'not_found']
81
+
82
+ export function ActionTrigger<TInput, TData>({
83
+ action,
84
+ input,
85
+ label,
86
+ variant,
87
+ size = 'default',
88
+ disabled = false,
89
+ confirm: confirmProp,
90
+ onSuccess,
91
+ icon,
92
+ itemLabel,
93
+ className,
94
+ }: ActionTriggerProps<TInput, TData>) {
95
+ const [open, setOpen] = useState(false)
96
+ const { trigger, isLoading } = useTriggerAction(action, {
97
+ onSuccess: (data) => {
98
+ toast.success(msgText(action.messages?.success, 'Concluído'))
99
+ onSuccess?.(data)
100
+ setOpen(false)
101
+ },
102
+ onError: (err) => {
103
+ const detail = err.message.trim()
104
+ const speakable = BUSINESS_ERRORS.includes(err.category as string) && detail !== ''
105
+ toast.error(speakable ? detail : msgText(action.messages?.error, err.message))
106
+ },
107
+ })
108
+
109
+ const buttonLabel = label ?? (typeof action.label === 'string' ? action.label : action.name)
110
+
111
+ // Prop sobrepõe; sem prop, o ConfirmSpec DECLARADO no contrato (action.confirm) vale.
112
+ const spec = action.confirm
113
+ const confirm =
114
+ confirmProp ??
115
+ (spec !== undefined
116
+ ? {
117
+ title: msgText(spec.title, 'Confirmar?'),
118
+ ...(spec.message !== undefined ? { description: msgText(spec.message, '') } : {}),
119
+ ...(spec.confirmLabel !== undefined ? { actionLabel: msgText(spec.confirmLabel, buttonLabel) } : {}),
120
+ ...(spec.cancelLabel !== undefined ? { cancelLabel: msgText(spec.cancelLabel, 'Cancelar') } : {}),
121
+ }
122
+ : undefined)
123
+ const destructive = spec?.destructive === true
124
+ // Com ícone o botão é chrome do item: fica ghost, e o vermelho aparece no hover — uma
125
+ // lixeira sólida vermelha em cada linha seria um campo minado visual.
126
+ const effectiveVariant = variant ?? (icon !== undefined ? 'ghost' : destructive ? 'destructive' : 'default')
127
+
128
+ const fire = () => {
129
+ void trigger(input)
130
+ }
131
+
132
+ /** O gatilho. Com `icon`: icon-only, rótulo no tooltip e no aria-label. O
133
+ * stopPropagation é o que permite viver DENTRO de uma linha clicável — sem ele,
134
+ * excluir também navegaria pro detalhe do que se está excluindo. */
135
+ const renderButton = (onClick: () => void): React.ReactElement => {
136
+ const button = (
137
+ <Button
138
+ variant={effectiveVariant}
139
+ size={icon !== undefined ? 'icon' : size}
140
+ busy={isLoading}
141
+ disabled={disabled}
142
+ aria-label={icon !== undefined ? buttonLabel : undefined}
143
+ onClick={(e) => {
144
+ e.stopPropagation()
145
+ onClick()
146
+ }}
147
+ className={cn(
148
+ icon !== undefined && 'size-7 shrink-0 text-muted-foreground/60',
149
+ icon !== undefined && destructive && 'hover:bg-destructive/10 hover:text-destructive',
150
+ className,
151
+ )}
152
+ data-action={action.name}
153
+ >
154
+ {icon ?? buttonLabel}
155
+ </Button>
156
+ )
157
+ if (icon === undefined) return button
158
+ return (
159
+ <Tooltip>
160
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
161
+ <TooltipContent>{buttonLabel}</TooltipContent>
162
+ </Tooltip>
163
+ )
164
+ }
165
+
166
+ if (confirm === undefined) return renderButton(fire)
167
+
168
+ return (
169
+ <>
170
+ {renderButton(() => setOpen(true))}
171
+
172
+ <AlertDialog open={open} onOpenChange={setOpen}>
173
+ <AlertDialogContent data-action={action.name}>
174
+ <AlertDialogHeader>
175
+ <AlertDialogTitle>{confirm.title}</AlertDialogTitle>
176
+ {(itemLabel !== undefined || confirm.description !== undefined) && (
177
+ <AlertDialogDescription>
178
+ {itemLabel !== undefined && <span className="font-medium text-foreground">“{itemLabel}”</span>}
179
+ {itemLabel !== undefined && confirm.description !== undefined ? ' — ' : ''}
180
+ {confirm.description}
181
+ </AlertDialogDescription>
182
+ )}
183
+ </AlertDialogHeader>
184
+ <AlertDialogFooter>
185
+ <Button variant="outline" onClick={() => setOpen(false)} disabled={isLoading}>
186
+ {confirm.cancelLabel ?? 'Cancelar'}
187
+ </Button>
188
+ <Button variant={effectiveVariant} busy={isLoading} onClick={fire}>
189
+ {confirm.actionLabel ?? buttonLabel}
190
+ </Button>
191
+ </AlertDialogFooter>
192
+ </AlertDialogContent>
193
+ </AlertDialog>
194
+ </>
195
+ )
196
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * <ActionView action input /> — carrega + exibe um recurso via ViewAction.
3
+ *
4
+ * Wrapper genérico sobre useViewAction: loading / error / empty states + delega o
5
+ * "data feliz" pra `children` (render prop `(data, refetch) => nó` — o layout é de quem
6
+ * compõe). `render` segue como alias. Emite `data-action="<action.name>"` na raiz (E2E).
7
+ */
8
+
9
+ import type { ReactNode } from 'react'
10
+ import type { ViewAction } from '../../../core/index.ts'
11
+ import { useViewAction } from '../../drivers/react.tsx'
12
+ import { Skeleton } from '../primitives/skeleton.tsx'
13
+ import { Button } from '../primitives/button.tsx'
14
+
15
+ export interface ActionViewProps<TInput, TData> {
16
+ action: ViewAction<TInput, TData>
17
+ input: TInput
18
+ /** Renderiza o "tem dado" — o layout é de quem compõe (composição, como no ActionForm). */
19
+ children?: (data: TData, refetch: () => Promise<void>) => ReactNode
20
+ /** Alias de children (a API original). Children tem precedência. */
21
+ render?: (data: TData, refetch: () => Promise<void>) => ReactNode
22
+ /** Renderiza estado vazio (200 mas sem dado). Default: nada. */
23
+ empty?: ReactNode
24
+ /** Renderiza loading. Default: 3 skeletons stackados. */
25
+ loading?: ReactNode
26
+ /** Renderiza erro. Default: mensagem + botão "Tentar de novo". */
27
+ error?: (err: { code: string; message: string }, retry: () => Promise<void>) => ReactNode
28
+ }
29
+
30
+ export function ActionView<TInput, TData>({
31
+ action,
32
+ input,
33
+ children,
34
+ render,
35
+ empty,
36
+ loading,
37
+ error,
38
+ }: ActionViewProps<TInput, TData>) {
39
+ const { data, isLoading, isError, error: err, refetch } = useViewAction<TInput, TData>(action, input)
40
+ const renderData = children ?? render
41
+ if (renderData === undefined) {
42
+ throw new Error('ActionView precisa de children (ou render) pra exibir o dado.')
43
+ }
44
+
45
+ if (isLoading) {
46
+ return (
47
+ <div data-action={action.name}>
48
+ {loading ?? (
49
+ <div className="space-y-3">
50
+ <Skeleton className="h-6 w-1/3" />
51
+ <Skeleton className="h-4 w-2/3" />
52
+ <Skeleton className="h-4 w-1/2" />
53
+ </div>
54
+ )}
55
+ </div>
56
+ )
57
+ }
58
+
59
+ if (isError && err !== undefined) {
60
+ if (error !== undefined) {
61
+ return <div data-action={action.name}>{error(err, refetch)}</div>
62
+ }
63
+ return (
64
+ <div
65
+ data-action={action.name}
66
+ className="rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm space-y-3"
67
+ >
68
+ <div>
69
+ <strong className="text-destructive">{err.code}</strong>
70
+ <p className="text-destructive">{err.message}</p>
71
+ </div>
72
+ <Button variant="outline" size="sm" onClick={() => void refetch()}>
73
+ Tentar de novo
74
+ </Button>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ if (data === undefined) {
80
+ return <div data-action={action.name}>{empty ?? null}</div>
81
+ }
82
+
83
+ return <div data-action={action.name}>{renderData(data, refetch)}</div>
84
+ }
@@ -0,0 +1,64 @@
1
+ import * as React from "react"
2
+ import { ChevronDownIcon } from "lucide-react"
3
+ import { Accordion as AccordionPrimitive } from "radix-ui"
4
+
5
+ import { cn } from '../../lib/cn.ts'
6
+
7
+ function Accordion({
8
+ ...props
9
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
10
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
11
+ }
12
+
13
+ function AccordionItem({
14
+ className,
15
+ ...props
16
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
17
+ return (
18
+ <AccordionPrimitive.Item
19
+ data-slot="accordion-item"
20
+ className={cn("border-b last:border-b-0", className)}
21
+ {...props}
22
+ />
23
+ )
24
+ }
25
+
26
+ function AccordionTrigger({
27
+ className,
28
+ children,
29
+ ...props
30
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
31
+ return (
32
+ <AccordionPrimitive.Header className="flex">
33
+ <AccordionPrimitive.Trigger
34
+ data-slot="accordion-trigger"
35
+ className={cn(
36
+ "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
43
+ </AccordionPrimitive.Trigger>
44
+ </AccordionPrimitive.Header>
45
+ )
46
+ }
47
+
48
+ function AccordionContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
53
+ return (
54
+ <AccordionPrimitive.Content
55
+ data-slot="accordion-content"
56
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
57
+ {...props}
58
+ >
59
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
60
+ </AccordionPrimitive.Content>
61
+ )
62
+ }
63
+
64
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,190 @@
1
+ import * as React from "react"
2
+ import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
3
+
4
+ import { cn } from '../../lib/cn.ts'
5
+ import { Button } from './button.tsx'
6
+
7
+ function AlertDialog({
8
+ ...props
9
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
10
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
11
+ }
12
+
13
+ function AlertDialogTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
16
+ return (
17
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
18
+ )
19
+ }
20
+
21
+ function AlertDialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
24
+ return (
25
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
26
+ )
27
+ }
28
+
29
+ function AlertDialogOverlay({
30
+ className,
31
+ ...props
32
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
33
+ return (
34
+ <AlertDialogPrimitive.Overlay
35
+ data-slot="alert-dialog-overlay"
36
+ className={cn(
37
+ "fixed inset-0 z-50 bg-black/30 backdrop-blur-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ function AlertDialogContent({
46
+ className,
47
+ ...props
48
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
49
+ return (
50
+ <AlertDialogPortal>
51
+ <AlertDialogOverlay />
52
+ <AlertDialogPrimitive.Content
53
+ data-slot="alert-dialog-content"
54
+ className={cn(
55
+ "group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-xs translate-x-[-50%] translate-y-[-50%] gap-4 rounded-dialog ring-1 ring-edge bg-background p-6 shadow-dialog duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ </AlertDialogPortal>
61
+ )
62
+ }
63
+
64
+ function AlertDialogHeader({
65
+ className,
66
+ ...props
67
+ }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div
70
+ data-slot="alert-dialog-header"
71
+ className={cn(
72
+ "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr]",
73
+ className
74
+ )}
75
+ {...props}
76
+ />
77
+ )
78
+ }
79
+
80
+ function AlertDialogFooter({
81
+ className,
82
+ ...props
83
+ }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="alert-dialog-footer"
87
+ className={cn(
88
+ "grid grid-cols-2 gap-2",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ )
94
+ }
95
+
96
+ function AlertDialogTitle({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
100
+ return (
101
+ <AlertDialogPrimitive.Title
102
+ data-slot="alert-dialog-title"
103
+ className={cn(
104
+ "text-lg font-semibold",
105
+ className
106
+ )}
107
+ {...props}
108
+ />
109
+ )
110
+ }
111
+
112
+ function AlertDialogDescription({
113
+ className,
114
+ ...props
115
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
116
+ return (
117
+ <AlertDialogPrimitive.Description
118
+ data-slot="alert-dialog-description"
119
+ className={cn("text-sm text-muted-foreground", className)}
120
+ {...props}
121
+ />
122
+ )
123
+ }
124
+
125
+ function AlertDialogMedia({
126
+ className,
127
+ ...props
128
+ }: React.ComponentProps<"div">) {
129
+ return (
130
+ <div
131
+ data-slot="alert-dialog-media"
132
+ className={cn(
133
+ "mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted *:[svg:not([class*='size-'])]:size-8",
134
+ className
135
+ )}
136
+ {...props}
137
+ />
138
+ )
139
+ }
140
+
141
+ function AlertDialogAction({
142
+ className,
143
+ variant = "default",
144
+ size = "default",
145
+ ...props
146
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
147
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
148
+ return (
149
+ <Button variant={variant} size={size} asChild>
150
+ <AlertDialogPrimitive.Action
151
+ data-slot="alert-dialog-action"
152
+ className={cn(className)}
153
+ {...props}
154
+ />
155
+ </Button>
156
+ )
157
+ }
158
+
159
+ function AlertDialogCancel({
160
+ className,
161
+ variant = "outline",
162
+ size = "default",
163
+ ...props
164
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
165
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
166
+ return (
167
+ <Button variant={variant} size={size} asChild>
168
+ <AlertDialogPrimitive.Cancel
169
+ data-slot="alert-dialog-cancel"
170
+ className={cn(className)}
171
+ {...props}
172
+ />
173
+ </Button>
174
+ )
175
+ }
176
+
177
+ export {
178
+ AlertDialog,
179
+ AlertDialogAction,
180
+ AlertDialogCancel,
181
+ AlertDialogContent,
182
+ AlertDialogDescription,
183
+ AlertDialogFooter,
184
+ AlertDialogHeader,
185
+ AlertDialogMedia,
186
+ AlertDialogOverlay,
187
+ AlertDialogPortal,
188
+ AlertDialogTitle,
189
+ AlertDialogTrigger,
190
+ }
@@ -0,0 +1,116 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import { cn } from '../../lib/cn.ts'
4
+
5
+ /**
6
+ * Variantes do alert (cva). COM ícone o layout é grid (1ª coluna pro `>svg`, 2ª pro
7
+ * conteúdo, que se ancora em `col-start-2`); sem ícone é bloco comum.
8
+ * `destructive` e `success` mantêm superfície tonalizada + borda (divergência da
9
+ * casa — o shadcn rebaixou destructive pra bg-card, nós preservamos o realce).
10
+ */
11
+ export const alertVariants = cva(
12
+ // O grid (coluna do ícone + coluna do conteúdo) só existe QUANDO HÁ ícone. Era fixo, com
13
+ // a 1ª coluna em `0` — e aí qualquer nó de texto solto virava item anônimo NESSA coluna
14
+ // de largura zero, saindo uma palavra por linha. Sem ícone, bloco comum: o `col-start-2`
15
+ // dos slots é inócuo fora de grid.
16
+ "relative w-full rounded-lg border px-4 py-3 text-sm has-[>svg]:grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:items-start has-[>svg]:gap-x-3 has-[>svg]:gap-y-0.5 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
17
+ {
18
+ variants: {
19
+ variant: {
20
+ default: 'bg-card text-card-foreground',
21
+ destructive:
22
+ 'border-destructive/50 bg-destructive/5 text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current',
23
+ success:
24
+ 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 *:data-[slot=alert-description]:text-emerald-700/90 dark:*:data-[slot=alert-description]:text-emerald-400/90 [&>svg]:text-current',
25
+ },
26
+ },
27
+ defaultVariants: { variant: 'default' },
28
+ },
29
+ )
30
+
31
+ /** `title` aqui é o TÍTULO do alert, não o atributo nativo do `<div>` (que é tooltip de
32
+ * browser — banido na casa em favor do `Tooltip`). Por isso o Omit. */
33
+ export interface AlertProps
34
+ extends Omit<React.ComponentProps<'div'>, 'title'>,
35
+ VariantProps<typeof alertVariants> {
36
+ /** Forma CURTA: com `title` (e/ou `description`) o alert se monta sozinho — o caso
37
+ * comum é uma linha só. Sem eles, vale a composição (`AlertTitle`/`AlertDescription`),
38
+ * que segue existindo pro conteúdo rico (lista, link, botão). */
39
+ title?: React.ReactNode
40
+ description?: React.ReactNode
41
+ /** Ícone à esquerda (a 1ª coluna do grid). Decorativo — quem nomeia é o título. */
42
+ icon?: React.ReactNode
43
+ }
44
+
45
+ export function Alert({
46
+ className,
47
+ variant,
48
+ title,
49
+ description,
50
+ icon,
51
+ children,
52
+ ...props
53
+ }: AlertProps): React.ReactElement {
54
+ const short = title !== undefined || description !== undefined
55
+ // Texto CRU como filho (`<Alert>Sincronizado.</Alert>`) vira descrição: no layout com
56
+ // ícone ele cairia na coluna do ícone, espremido. Assim o atalho de sempre continua
57
+ // valendo e cai no slot certo.
58
+ const raw = typeof children === 'string' || typeof children === 'number'
59
+ const body = raw ? <AlertDescription>{children}</AlertDescription> : children
60
+ return (
61
+ <div
62
+ data-slot="alert"
63
+ role="alert"
64
+ className={cn(alertVariants({ variant }), className)}
65
+ {...props}
66
+ >
67
+ {icon}
68
+ {short ? (
69
+ <>
70
+ {title !== undefined && <AlertTitle>{title}</AlertTitle>}
71
+ {description !== undefined && <AlertDescription>{description}</AlertDescription>}
72
+ {/* A ação (children DEPOIS da frase) precisa da coluna 2: sem `col-start-2`
73
+ ela cai na coluna do ícone (~1rem) e é espremida pra esquerda. O
74
+ `col-start-2` é inócuo sem o grid (forma curta sem ícone). */}
75
+ {children !== undefined && children !== null && (
76
+ <div data-slot="alert-actions" className="col-start-2 mt-1">
77
+ {body}
78
+ </div>
79
+ )}
80
+ </>
81
+ ) : (
82
+ body
83
+ )}
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export function AlertTitle({ className, ...props }: React.ComponentProps<'div'>): React.ReactElement {
89
+ return (
90
+ <div
91
+ data-slot="alert-title"
92
+ className={cn('col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight', className)}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ export function AlertDescription({
99
+ className,
100
+ ...props
101
+ }: React.ComponentProps<'div'>): React.ReactElement {
102
+ return (
103
+ <div
104
+ data-slot="alert-description"
105
+ className={cn(
106
+ // NÃO é grid (era, no shadcn): num grid TODO nó filho vira item empilhado, então
107
+ // uma frase com interpolação — `Workspace {cliente} {nome} sincronizado.` — saía
108
+ // uma palavra por linha. Texto inline flui; o espaço entre BLOCOS filhos (p, ul,
109
+ // ação) continua vindo do `*+*`.
110
+ 'col-start-2 text-sm text-muted-foreground [&>*+*]:mt-1 [&_p]:leading-relaxed',
111
+ className,
112
+ )}
113
+ {...props}
114
+ />
115
+ )
116
+ }
@@ -0,0 +1,9 @@
1
+ import { AspectRatio as AspectRatioPrimitive } from "radix-ui"
2
+
3
+ function AspectRatio({
4
+ ...props
5
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
6
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
7
+ }
8
+
9
+ export { AspectRatio }