braid-ui 1.0.98 → 1.0.99
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/README.md +44 -327
- package/components.json +20 -0
- package/eslint.config.js +29 -0
- package/index.html +24 -0
- package/package.json +55 -115
- package/postcss.config.js +6 -0
- package/public/favicon.ico +0 -0
- package/public/placeholder.svg +1 -0
- package/public/robots.txt +14 -0
- package/src/App.css +42 -0
- package/src/App.tsx +94 -0
- package/src/components/MainLayout.tsx +15 -0
- package/src/components/alerts/AlertDocuments.tsx +320 -0
- package/src/components/alerts/AlertNotes.tsx +185 -0
- package/src/components/alerts/AlertTimeline.tsx +79 -0
- package/src/components/alerts/ContextSection.tsx +155 -0
- package/src/components/app-sidebar.tsx +341 -0
- package/src/components/form-sections/ACHBankCard.tsx +78 -0
- package/src/components/form-sections/ACHBasicInfoCard.tsx +100 -0
- package/src/components/form-sections/ACHTransferSection.tsx +64 -0
- package/src/components/form-sections/AddressForm.tsx +94 -0
- package/src/components/form-sections/BankAddressCard.tsx +95 -0
- package/src/components/form-sections/BankingDetailsCard.tsx +46 -0
- package/src/components/form-sections/BasicInfoCard.tsx +103 -0
- package/src/components/form-sections/BasicInfoSection.tsx +34 -0
- package/src/components/form-sections/BeneficiaryAddress.tsx +19 -0
- package/src/components/form-sections/BeneficiaryCard.tsx +41 -0
- package/src/components/form-sections/BeneficiaryDomesticWire.tsx +23 -0
- package/src/components/form-sections/BusinessProfileCard.tsx +131 -0
- package/src/components/form-sections/BusinessStatusCard.tsx +53 -0
- package/src/components/form-sections/ContactInfoCard.tsx +63 -0
- package/src/components/form-sections/CounterpartyBasicInfo.tsx +101 -0
- package/src/components/form-sections/CounterpartyProfileCard.tsx +104 -0
- package/src/components/form-sections/CounterpartyRecordsCard.tsx +41 -0
- package/src/components/form-sections/IntermediaryCard.tsx +77 -0
- package/src/components/form-sections/IntermediaryFI.tsx +41 -0
- package/src/components/form-sections/IntermediaryFIAddress.tsx +14 -0
- package/src/components/form-sections/OriginatorCard.tsx +49 -0
- package/src/components/form-sections/OriginatorFI.tsx +42 -0
- package/src/components/form-sections/OriginatorFIAddress.tsx +14 -0
- package/src/components/form-sections/PaymentInformationSection.tsx +163 -0
- package/src/components/form-sections/ReceiverCard.tsx +94 -0
- package/src/components/form-sections/WireTransferSection.tsx +75 -0
- package/src/components/layouts/list-page.tsx +103 -0
- package/src/components/transaction/ACHDetailsSection.tsx +95 -0
- package/src/components/transaction/WireDetailsSection.tsx +112 -0
- package/src/components/ui/account-card.tsx +94 -0
- package/src/components/ui/badge.tsx +75 -0
- package/src/components/ui/breadcrumb.tsx +78 -0
- package/src/components/ui/business-type-badge.tsx +42 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/calendar.tsx +49 -0
- package/src/components/ui/card.tsx +223 -0
- package/src/components/ui/container.tsx +45 -0
- package/src/components/ui/counterparty-type-badge.tsx +53 -0
- package/src/components/ui/data-grid.tsx +99 -0
- package/src/components/ui/data-table.tsx +152 -0
- package/src/components/ui/detail-page-layout.tsx +83 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/dropdown-menu.tsx +82 -0
- package/src/components/ui/editable-form-card.tsx +106 -0
- package/src/components/ui/editable-info-field.tsx +67 -0
- package/src/components/ui/enhanced-input.tsx +78 -0
- package/src/components/ui/enhanced-select.tsx +101 -0
- package/src/components/ui/enhanced-textarea.tsx +64 -0
- package/src/components/ui/entity-card.tsx +140 -0
- package/src/components/ui/form-card.tsx +40 -0
- package/src/components/ui/form-field.tsx +50 -0
- package/src/components/ui/form-input.tsx +29 -0
- package/src/components/ui/form-provider.tsx +18 -0
- package/src/components/ui/form-section.tsx +66 -0
- package/src/components/ui/form-select.tsx +35 -0
- package/src/components/ui/info-field.tsx +36 -0
- package/src/components/ui/json-viewer.tsx +146 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/metric-card.tsx +80 -0
- package/src/components/ui/page-layout.tsx +183 -0
- package/src/components/ui/popover.tsx +29 -0
- package/src/components/ui/responsive-grid.tsx +46 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +775 -0
- package/src/components/ui/sonner.tsx +29 -0
- package/src/components/ui/stack.tsx +77 -0
- package/src/components/ui/status-badge.tsx +68 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/toast.tsx +127 -0
- package/src/components/ui/toaster.tsx +33 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/ui/use-toast.ts +3 -0
- package/src/components/ui-kit/dashboard-demo.tsx +156 -0
- package/src/components/ui-kit/pattern-library.tsx +248 -0
- package/src/components/ui-kit/showcase.tsx +211 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-toast.ts +191 -0
- package/src/hooks/useEditState.ts +70 -0
- package/src/hooks/useFormWithEditState.ts +115 -0
- package/src/{styles.css → index.css} +10 -4
- package/src/lib/constants.ts +25 -0
- package/src/lib/mock-data/alert-data.ts +275 -0
- package/src/lib/mock-data/banking-data.ts +72 -0
- package/src/lib/mock-data/business-data.ts +71 -0
- package/src/lib/mock-data/counterparty-data.ts +70 -0
- package/src/lib/mock-data/index.ts +5 -0
- package/src/lib/mock-data/transaction-data.ts +283 -0
- package/src/lib/mock-data/wire-data.ts +103 -0
- package/src/lib/mock-data.tsx +180 -0
- package/src/lib/schemas/banking-schemas.ts +30 -0
- package/src/lib/schemas/business-schemas.ts +36 -0
- package/src/lib/schemas/counterparty-schemas.ts +43 -0
- package/src/lib/schemas/index.ts +5 -0
- package/src/lib/schemas/wire-schemas.ts +44 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/pages/Cases.tsx +16 -0
- package/src/pages/Dashboard.tsx +16 -0
- package/src/pages/NotFound.tsx +27 -0
- package/src/pages/TransactionHistory.tsx +532 -0
- package/src/pages/UIKit.tsx +51 -0
- package/src/pages/alerts/AlertDetail.tsx +193 -0
- package/src/pages/alerts/Alerts.tsx +373 -0
- package/src/pages/business/Business.tsx +48 -0
- package/src/pages/business/Create.tsx +173 -0
- package/src/pages/counterparty/Create.tsx +48 -0
- package/src/pages/counterparty/DomesticWire.tsx +78 -0
- package/src/pages/counterparty/Manage.tsx +79 -0
- package/src/pages/transactions/NewTransaction.tsx +527 -0
- package/src/pages/transactions/TransactionDetail.tsx +192 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +124 -0
- package/tsconfig.app.json +30 -0
- package/tsconfig.json +19 -0
- package/tsconfig.node.json +22 -0
- package/vite.config.ts +22 -0
- package/dist/css/braid-ui-variables.css +0 -88
- package/dist/css/braid-ui.css +0 -4484
- package/dist/css/braid-ui.min.css +0 -1
- package/dist/index.cjs +0 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2429
- package/dist/index.d.ts +0 -2429
- package/dist/index.js +0 -4
- package/dist/index.js.map +0 -1
- package/src/styles-only.css +0 -121
- /package/{dist/braid-logo-343BOQZ2.png → src/assets/braid-logo.png} +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
+
import { X } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const Dialog = DialogPrimitive.Root
|
|
8
|
+
|
|
9
|
+
const DialogTrigger = DialogPrimitive.Trigger
|
|
10
|
+
|
|
11
|
+
const DialogPortal = DialogPrimitive.Portal
|
|
12
|
+
|
|
13
|
+
const DialogClose = DialogPrimitive.Close
|
|
14
|
+
|
|
15
|
+
const DialogOverlay = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
17
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
18
|
+
>(({ className, ...props }, ref) => (
|
|
19
|
+
<DialogPrimitive.Overlay
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn(
|
|
22
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
))
|
|
28
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
29
|
+
|
|
30
|
+
const DialogContent = React.forwardRef<
|
|
31
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
32
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
33
|
+
>(({ className, children, ...props }, ref) => (
|
|
34
|
+
<DialogPortal>
|
|
35
|
+
<DialogOverlay />
|
|
36
|
+
<DialogPrimitive.Content
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-4xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg max-h-[90vh] overflow-hidden",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
|
46
|
+
<X className="h-4 w-4" />
|
|
47
|
+
<span className="sr-only">Close</span>
|
|
48
|
+
</DialogPrimitive.Close>
|
|
49
|
+
</DialogPrimitive.Content>
|
|
50
|
+
</DialogPortal>
|
|
51
|
+
))
|
|
52
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
53
|
+
|
|
54
|
+
const DialogHeader = ({
|
|
55
|
+
className,
|
|
56
|
+
...props
|
|
57
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
DialogHeader.displayName = "DialogHeader"
|
|
67
|
+
|
|
68
|
+
const DialogFooter = ({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
DialogFooter.displayName = "DialogFooter"
|
|
81
|
+
|
|
82
|
+
const DialogTitle = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<DialogPrimitive.Title
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn(
|
|
89
|
+
"text-lg font-semibold leading-none tracking-tight",
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
))
|
|
95
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
96
|
+
|
|
97
|
+
const DialogDescription = React.forwardRef<
|
|
98
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
99
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
100
|
+
>(({ className, ...props }, ref) => (
|
|
101
|
+
<DialogPrimitive.Description
|
|
102
|
+
ref={ref}
|
|
103
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
))
|
|
107
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
Dialog,
|
|
111
|
+
DialogPortal,
|
|
112
|
+
DialogOverlay,
|
|
113
|
+
DialogClose,
|
|
114
|
+
DialogTrigger,
|
|
115
|
+
DialogContent,
|
|
116
|
+
DialogHeader,
|
|
117
|
+
DialogFooter,
|
|
118
|
+
DialogTitle,
|
|
119
|
+
DialogDescription,
|
|
120
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
6
|
+
|
|
7
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
8
|
+
|
|
9
|
+
const DropdownMenuContent = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
12
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
13
|
+
<DropdownMenuPrimitive.Portal>
|
|
14
|
+
<DropdownMenuPrimitive.Content
|
|
15
|
+
ref={ref}
|
|
16
|
+
sideOffset={sideOffset}
|
|
17
|
+
className={cn(
|
|
18
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
</DropdownMenuPrimitive.Portal>
|
|
24
|
+
))
|
|
25
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
26
|
+
|
|
27
|
+
const DropdownMenuItem = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
29
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
30
|
+
inset?: boolean
|
|
31
|
+
}
|
|
32
|
+
>(({ className, inset, ...props }, ref) => (
|
|
33
|
+
<DropdownMenuPrimitive.Item
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn(
|
|
36
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
37
|
+
inset && "pl-8",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
))
|
|
43
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
44
|
+
|
|
45
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
46
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
47
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
48
|
+
inset?: boolean
|
|
49
|
+
}
|
|
50
|
+
>(({ className, inset, ...props }, ref) => (
|
|
51
|
+
<DropdownMenuPrimitive.Label
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn(
|
|
54
|
+
"px-2 py-1.5 text-sm font-semibold",
|
|
55
|
+
inset && "pl-8",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
))
|
|
61
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
62
|
+
|
|
63
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
64
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
65
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
66
|
+
>(({ className, ...props }, ref) => (
|
|
67
|
+
<DropdownMenuPrimitive.Separator
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
))
|
|
73
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
DropdownMenu,
|
|
77
|
+
DropdownMenuTrigger,
|
|
78
|
+
DropdownMenuContent,
|
|
79
|
+
DropdownMenuItem,
|
|
80
|
+
DropdownMenuLabel,
|
|
81
|
+
DropdownMenuSeparator,
|
|
82
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { FormCard, type FormCardProps } from "@/components/ui/form-card"
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
import { Edit } from "lucide-react"
|
|
5
|
+
import { useEditState } from "@/hooks/useEditState"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
interface EditableFormCardProps extends Omit<FormCardProps, 'headerActions' | 'children'> {
|
|
9
|
+
title: string
|
|
10
|
+
description?: string
|
|
11
|
+
children?: React.ReactNode
|
|
12
|
+
editContent: React.ReactNode
|
|
13
|
+
viewContent: React.ReactNode
|
|
14
|
+
isEditing?: boolean
|
|
15
|
+
onToggleEdit?: () => void
|
|
16
|
+
hideActions?: boolean
|
|
17
|
+
onSave?: () => void | Promise<void>
|
|
18
|
+
onCancel?: () => void
|
|
19
|
+
className?: string
|
|
20
|
+
isFormValid?: boolean
|
|
21
|
+
isDirty?: boolean
|
|
22
|
+
isSubmitting?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const EditableFormCard = React.forwardRef<HTMLDivElement, EditableFormCardProps>(
|
|
26
|
+
({
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
editContent,
|
|
30
|
+
viewContent,
|
|
31
|
+
isEditing,
|
|
32
|
+
onToggleEdit,
|
|
33
|
+
hideActions = false,
|
|
34
|
+
onSave,
|
|
35
|
+
onCancel,
|
|
36
|
+
className,
|
|
37
|
+
variant = "subtle",
|
|
38
|
+
isFormValid = true,
|
|
39
|
+
isDirty = false,
|
|
40
|
+
isSubmitting = false,
|
|
41
|
+
...props
|
|
42
|
+
}, ref) => {
|
|
43
|
+
const { isEditing: currentlyEditing, handleToggleEdit, handleCancel } = useEditState({
|
|
44
|
+
initialEditing: isEditing ?? false,
|
|
45
|
+
onToggleEdit
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const handleSaveAction = async () => {
|
|
49
|
+
if (onSave) {
|
|
50
|
+
await onSave()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleCancelAction = () => {
|
|
55
|
+
if (onCancel) {
|
|
56
|
+
onCancel()
|
|
57
|
+
} else {
|
|
58
|
+
handleCancel()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<FormCard
|
|
64
|
+
ref={ref}
|
|
65
|
+
title={title}
|
|
66
|
+
description={description}
|
|
67
|
+
variant={variant}
|
|
68
|
+
className={className}
|
|
69
|
+
headerActions={
|
|
70
|
+
!hideActions ? (
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
{currentlyEditing ? (
|
|
73
|
+
<>
|
|
74
|
+
<Button variant="outline" size="sm" onClick={handleCancelAction}>
|
|
75
|
+
Cancel
|
|
76
|
+
</Button>
|
|
77
|
+
<Button
|
|
78
|
+
size="sm"
|
|
79
|
+
onClick={handleSaveAction}
|
|
80
|
+
disabled={!isFormValid || isSubmitting}
|
|
81
|
+
>
|
|
82
|
+
{isSubmitting ? "Saving..." : "Save"}
|
|
83
|
+
</Button>
|
|
84
|
+
</>
|
|
85
|
+
) : (
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="icon"
|
|
89
|
+
onClick={handleToggleEdit}
|
|
90
|
+
className="text-primary hover:text-primary/80 hover:bg-primary/10"
|
|
91
|
+
>
|
|
92
|
+
<Edit className="h-4 w-4" />
|
|
93
|
+
</Button>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
) : undefined
|
|
97
|
+
}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
{currentlyEditing ? editContent : viewContent}
|
|
101
|
+
</FormCard>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
EditableFormCard.displayName = "EditableFormCard"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { InfoField } from "./info-field"
|
|
3
|
+
import { EnhancedSelect } from "./enhanced-select"
|
|
4
|
+
import { Badge } from "./badge"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
interface EditableInfoFieldProps {
|
|
8
|
+
label: string
|
|
9
|
+
value: string | null | undefined
|
|
10
|
+
options: { value: string; label: string }[]
|
|
11
|
+
onChange: (value: string) => void
|
|
12
|
+
placeholder?: string
|
|
13
|
+
renderValue?: (value: string) => React.ReactNode
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const EditableInfoField = ({
|
|
18
|
+
label,
|
|
19
|
+
value,
|
|
20
|
+
options,
|
|
21
|
+
onChange,
|
|
22
|
+
placeholder = "Select...",
|
|
23
|
+
renderValue,
|
|
24
|
+
className
|
|
25
|
+
}: EditableInfoFieldProps) => {
|
|
26
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
27
|
+
|
|
28
|
+
const handleChange = (newValue: string) => {
|
|
29
|
+
onChange(newValue)
|
|
30
|
+
setIsEditing(false)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isEditing) {
|
|
34
|
+
return (
|
|
35
|
+
<div className={className}>
|
|
36
|
+
<EnhancedSelect
|
|
37
|
+
label={label}
|
|
38
|
+
value={value || ""}
|
|
39
|
+
onValueChange={handleChange}
|
|
40
|
+
options={options}
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const displayValue = value
|
|
48
|
+
? (renderValue ? renderValue(value) : value)
|
|
49
|
+
: placeholder
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={cn("cursor-pointer transition-colors hover:bg-muted/50 rounded-md -mx-2 px-2 -my-1 py-1", className)}
|
|
54
|
+
onClick={() => setIsEditing(true)}
|
|
55
|
+
role="button"
|
|
56
|
+
tabIndex={0}
|
|
57
|
+
onKeyDown={(e) => {
|
|
58
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
setIsEditing(true)
|
|
61
|
+
}
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<InfoField label={label} value={displayValue} />
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
const inputVariants = cva(
|
|
6
|
+
"flex w-full rounded-md bg-form-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground transition-form",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "border border-form-border focus-visible:border-form-border-focus focus-visible:outline-none focus-visible:shadow-form-focus",
|
|
11
|
+
error: "border border-form-border-error focus-visible:border-form-border-error focus-visible:outline-none focus-visible:shadow-form-error",
|
|
12
|
+
success: "border border-form-border-success focus-visible:border-form-border-success focus-visible:outline-none",
|
|
13
|
+
disabled: "border border-form-border bg-muted cursor-not-allowed opacity-50",
|
|
14
|
+
readonly: "border border-form-border bg-muted/50 cursor-default"
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: "h-10",
|
|
18
|
+
sm: "h-9",
|
|
19
|
+
lg: "h-11"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: "default",
|
|
24
|
+
size: "default"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export interface InputProps
|
|
30
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
|
|
31
|
+
VariantProps<typeof inputVariants> {
|
|
32
|
+
label?: string
|
|
33
|
+
hint?: string
|
|
34
|
+
error?: string
|
|
35
|
+
success?: string
|
|
36
|
+
isLoading?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const EnhancedInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|
40
|
+
({ className, variant, size, label, hint, error, success, isLoading, ...props }, ref) => {
|
|
41
|
+
const inputVariant = error ? "error" : success ? "success" : props.disabled ? "disabled" : props.readOnly ? "readonly" : variant
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="space-y-2">
|
|
45
|
+
{label && (
|
|
46
|
+
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
47
|
+
{label}
|
|
48
|
+
{props.required && <span className="text-destructive ml-1">*</span>}
|
|
49
|
+
</label>
|
|
50
|
+
)}
|
|
51
|
+
<div className="relative">
|
|
52
|
+
<input
|
|
53
|
+
className={cn(inputVariants({ variant: inputVariant, size, className }))}
|
|
54
|
+
ref={ref}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
{isLoading && (
|
|
58
|
+
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
|
|
59
|
+
<div className="animate-spin rounded-full h-4 w-4 border-2 border-primary border-t-transparent"></div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
{hint && !error && !success && (
|
|
64
|
+
<p className="text-xs text-muted-foreground">{hint}</p>
|
|
65
|
+
)}
|
|
66
|
+
{error && (
|
|
67
|
+
<p className="text-xs text-destructive">{error}</p>
|
|
68
|
+
)}
|
|
69
|
+
{success && (
|
|
70
|
+
<p className="text-xs text-success">{success}</p>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
EnhancedInput.displayName = "EnhancedInput"
|
|
77
|
+
|
|
78
|
+
export { EnhancedInput, inputVariants }
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
3
|
+
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
6
|
+
|
|
7
|
+
const selectVariants = cva(
|
|
8
|
+
"flex h-10 w-full items-center justify-between rounded-md bg-form-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none transition-form",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "border border-form-border focus:border-form-border-focus focus:shadow-form-focus",
|
|
13
|
+
error: "border border-form-border-error focus:border-form-border-error focus:shadow-form-error",
|
|
14
|
+
success: "border border-form-border-success focus:border-form-border-success",
|
|
15
|
+
disabled: "border border-form-border bg-muted cursor-not-allowed opacity-50",
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: {
|
|
19
|
+
variant: "default"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
interface EnhancedSelectProps extends VariantProps<typeof selectVariants> {
|
|
25
|
+
label?: string
|
|
26
|
+
hint?: string
|
|
27
|
+
error?: string
|
|
28
|
+
success?: string
|
|
29
|
+
placeholder?: string
|
|
30
|
+
options: { value: string; label: string; disabled?: boolean }[]
|
|
31
|
+
value?: string
|
|
32
|
+
onValueChange?: (value: string) => void
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const EnhancedSelect = React.forwardRef<
|
|
37
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
38
|
+
EnhancedSelectProps
|
|
39
|
+
>(({ variant, label, hint, error, success, placeholder, options, disabled, ...props }, ref) => {
|
|
40
|
+
const selectVariant = error ? "error" : success ? "success" : disabled ? "disabled" : variant
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="space-y-2">
|
|
44
|
+
{label && (
|
|
45
|
+
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
46
|
+
{label}
|
|
47
|
+
</label>
|
|
48
|
+
)}
|
|
49
|
+
<SelectPrimitive.Root {...props} disabled={disabled}>
|
|
50
|
+
<SelectPrimitive.Trigger
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn(selectVariants({ variant: selectVariant }))}
|
|
53
|
+
>
|
|
54
|
+
<SelectPrimitive.Value placeholder={placeholder} />
|
|
55
|
+
<SelectPrimitive.Icon asChild>
|
|
56
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
57
|
+
</SelectPrimitive.Icon>
|
|
58
|
+
</SelectPrimitive.Trigger>
|
|
59
|
+
<SelectPrimitive.Portal>
|
|
60
|
+
<SelectPrimitive.Content className="relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2">
|
|
61
|
+
<SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
|
|
62
|
+
<ChevronUp className="h-4 w-4" />
|
|
63
|
+
</SelectPrimitive.ScrollUpButton>
|
|
64
|
+
<SelectPrimitive.Viewport className="p-1">
|
|
65
|
+
{options.map((option) => (
|
|
66
|
+
<SelectPrimitive.Item
|
|
67
|
+
key={option.value}
|
|
68
|
+
value={option.value}
|
|
69
|
+
disabled={option.disabled}
|
|
70
|
+
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
|
71
|
+
>
|
|
72
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
73
|
+
<SelectPrimitive.ItemIndicator>
|
|
74
|
+
<Check className="h-4 w-4" />
|
|
75
|
+
</SelectPrimitive.ItemIndicator>
|
|
76
|
+
</span>
|
|
77
|
+
<SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>
|
|
78
|
+
</SelectPrimitive.Item>
|
|
79
|
+
))}
|
|
80
|
+
</SelectPrimitive.Viewport>
|
|
81
|
+
<SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
|
|
82
|
+
<ChevronDown className="h-4 w-4" />
|
|
83
|
+
</SelectPrimitive.ScrollDownButton>
|
|
84
|
+
</SelectPrimitive.Content>
|
|
85
|
+
</SelectPrimitive.Portal>
|
|
86
|
+
</SelectPrimitive.Root>
|
|
87
|
+
{hint && !error && !success && (
|
|
88
|
+
<p className="text-xs text-muted-foreground">{hint}</p>
|
|
89
|
+
)}
|
|
90
|
+
{error && (
|
|
91
|
+
<p className="text-xs text-destructive">{error}</p>
|
|
92
|
+
)}
|
|
93
|
+
{success && (
|
|
94
|
+
<p className="text-xs text-success">{success}</p>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
EnhancedSelect.displayName = "EnhancedSelect"
|
|
100
|
+
|
|
101
|
+
export { EnhancedSelect }
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
const textareaVariants = cva(
|
|
6
|
+
"flex min-h-[80px] w-full rounded-md bg-form-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground transition-form resize-y",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "border border-form-border focus-visible:border-form-border-focus focus-visible:outline-none focus-visible:shadow-form-focus",
|
|
11
|
+
error: "border border-form-border-error focus-visible:border-form-border-error focus-visible:outline-none focus-visible:shadow-form-error",
|
|
12
|
+
success: "border border-form-border-success focus-visible:border-form-border-success focus-visible:outline-none",
|
|
13
|
+
disabled: "border border-form-border bg-muted cursor-not-allowed opacity-50 resize-none",
|
|
14
|
+
readonly: "border border-form-border bg-muted/50 cursor-default resize-none"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: "default"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export interface TextareaProps
|
|
24
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
25
|
+
VariantProps<typeof textareaVariants> {
|
|
26
|
+
label?: string
|
|
27
|
+
hint?: string
|
|
28
|
+
error?: string
|
|
29
|
+
success?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const EnhancedTextarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
33
|
+
({ className, variant, label, hint, error, success, ...props }, ref) => {
|
|
34
|
+
const textareaVariant = error ? "error" : success ? "success" : props.disabled ? "disabled" : props.readOnly ? "readonly" : variant
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="space-y-2">
|
|
38
|
+
{label && (
|
|
39
|
+
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
40
|
+
{label}
|
|
41
|
+
{props.required && <span className="text-destructive ml-1">*</span>}
|
|
42
|
+
</label>
|
|
43
|
+
)}
|
|
44
|
+
<textarea
|
|
45
|
+
className={cn(textareaVariants({ variant: textareaVariant, className }))}
|
|
46
|
+
ref={ref}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
{hint && !error && !success && (
|
|
50
|
+
<p className="text-xs text-muted-foreground">{hint}</p>
|
|
51
|
+
)}
|
|
52
|
+
{error && (
|
|
53
|
+
<p className="text-xs text-destructive">{error}</p>
|
|
54
|
+
)}
|
|
55
|
+
{success && (
|
|
56
|
+
<p className="text-xs text-success">{success}</p>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
EnhancedTextarea.displayName = "EnhancedTextarea"
|
|
63
|
+
|
|
64
|
+
export { EnhancedTextarea, textareaVariants }
|