@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.
- package/CHANGELOG.md +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- 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
|
+
}
|