@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,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 }
|