braid-ui 1.0.97 → 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.
Files changed (145) hide show
  1. package/README.md +44 -327
  2. package/components.json +20 -0
  3. package/eslint.config.js +29 -0
  4. package/index.html +24 -0
  5. package/package.json +55 -115
  6. package/postcss.config.js +6 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/placeholder.svg +1 -0
  9. package/public/robots.txt +14 -0
  10. package/src/App.css +42 -0
  11. package/src/App.tsx +94 -0
  12. package/src/components/MainLayout.tsx +15 -0
  13. package/src/components/alerts/AlertDocuments.tsx +320 -0
  14. package/src/components/alerts/AlertNotes.tsx +185 -0
  15. package/src/components/alerts/AlertTimeline.tsx +79 -0
  16. package/src/components/alerts/ContextSection.tsx +155 -0
  17. package/src/components/app-sidebar.tsx +341 -0
  18. package/src/components/form-sections/ACHBankCard.tsx +78 -0
  19. package/src/components/form-sections/ACHBasicInfoCard.tsx +100 -0
  20. package/src/components/form-sections/ACHTransferSection.tsx +64 -0
  21. package/src/components/form-sections/AddressForm.tsx +94 -0
  22. package/src/components/form-sections/BankAddressCard.tsx +95 -0
  23. package/src/components/form-sections/BankingDetailsCard.tsx +46 -0
  24. package/src/components/form-sections/BasicInfoCard.tsx +103 -0
  25. package/src/components/form-sections/BasicInfoSection.tsx +34 -0
  26. package/src/components/form-sections/BeneficiaryAddress.tsx +19 -0
  27. package/src/components/form-sections/BeneficiaryCard.tsx +41 -0
  28. package/src/components/form-sections/BeneficiaryDomesticWire.tsx +23 -0
  29. package/src/components/form-sections/BusinessProfileCard.tsx +131 -0
  30. package/src/components/form-sections/BusinessStatusCard.tsx +53 -0
  31. package/src/components/form-sections/ContactInfoCard.tsx +63 -0
  32. package/src/components/form-sections/CounterpartyBasicInfo.tsx +101 -0
  33. package/src/components/form-sections/CounterpartyProfileCard.tsx +104 -0
  34. package/src/components/form-sections/CounterpartyRecordsCard.tsx +41 -0
  35. package/src/components/form-sections/IntermediaryCard.tsx +77 -0
  36. package/src/components/form-sections/IntermediaryFI.tsx +41 -0
  37. package/src/components/form-sections/IntermediaryFIAddress.tsx +14 -0
  38. package/src/components/form-sections/OriginatorCard.tsx +49 -0
  39. package/src/components/form-sections/OriginatorFI.tsx +42 -0
  40. package/src/components/form-sections/OriginatorFIAddress.tsx +14 -0
  41. package/src/components/form-sections/PaymentInformationSection.tsx +163 -0
  42. package/src/components/form-sections/ReceiverCard.tsx +94 -0
  43. package/src/components/form-sections/WireTransferSection.tsx +75 -0
  44. package/src/components/layouts/list-page.tsx +103 -0
  45. package/src/components/transaction/ACHDetailsSection.tsx +95 -0
  46. package/src/components/transaction/WireDetailsSection.tsx +112 -0
  47. package/src/components/ui/account-card.tsx +94 -0
  48. package/src/components/ui/badge.tsx +75 -0
  49. package/src/components/ui/breadcrumb.tsx +78 -0
  50. package/src/components/ui/business-type-badge.tsx +42 -0
  51. package/src/components/ui/button.tsx +56 -0
  52. package/src/components/ui/calendar.tsx +49 -0
  53. package/src/components/ui/card.tsx +223 -0
  54. package/src/components/ui/container.tsx +45 -0
  55. package/src/components/ui/counterparty-type-badge.tsx +53 -0
  56. package/src/components/ui/data-grid.tsx +99 -0
  57. package/src/components/ui/data-table.tsx +152 -0
  58. package/src/components/ui/detail-page-layout.tsx +83 -0
  59. package/src/components/ui/dialog.tsx +120 -0
  60. package/src/components/ui/dropdown-menu.tsx +82 -0
  61. package/src/components/ui/editable-form-card.tsx +106 -0
  62. package/src/components/ui/editable-info-field.tsx +67 -0
  63. package/src/components/ui/enhanced-input.tsx +78 -0
  64. package/src/components/ui/enhanced-select.tsx +101 -0
  65. package/src/components/ui/enhanced-textarea.tsx +64 -0
  66. package/src/components/ui/entity-card.tsx +140 -0
  67. package/src/components/ui/form-card.tsx +40 -0
  68. package/src/components/ui/form-field.tsx +50 -0
  69. package/src/components/ui/form-input.tsx +29 -0
  70. package/src/components/ui/form-provider.tsx +18 -0
  71. package/src/components/ui/form-section.tsx +66 -0
  72. package/src/components/ui/form-select.tsx +35 -0
  73. package/src/components/ui/info-field.tsx +36 -0
  74. package/src/components/ui/json-viewer.tsx +146 -0
  75. package/src/components/ui/label.tsx +24 -0
  76. package/src/components/ui/metric-card.tsx +80 -0
  77. package/src/components/ui/page-layout.tsx +183 -0
  78. package/src/components/ui/popover.tsx +29 -0
  79. package/src/components/ui/responsive-grid.tsx +46 -0
  80. package/src/components/ui/separator.tsx +31 -0
  81. package/src/components/ui/sheet.tsx +140 -0
  82. package/src/components/ui/sidebar.tsx +775 -0
  83. package/src/components/ui/sonner.tsx +29 -0
  84. package/src/components/ui/stack.tsx +77 -0
  85. package/src/components/ui/status-badge.tsx +68 -0
  86. package/src/components/ui/tabs.tsx +52 -0
  87. package/src/components/ui/toast.tsx +127 -0
  88. package/src/components/ui/toaster.tsx +33 -0
  89. package/src/components/ui/tooltip.tsx +28 -0
  90. package/src/components/ui/use-toast.ts +3 -0
  91. package/src/components/ui-kit/dashboard-demo.tsx +156 -0
  92. package/src/components/ui-kit/pattern-library.tsx +248 -0
  93. package/src/components/ui-kit/showcase.tsx +211 -0
  94. package/src/hooks/use-mobile.tsx +19 -0
  95. package/src/hooks/use-toast.ts +191 -0
  96. package/src/hooks/useEditState.ts +70 -0
  97. package/src/hooks/useFormWithEditState.ts +115 -0
  98. package/src/{styles.css → index.css} +10 -4
  99. package/src/lib/constants.ts +25 -0
  100. package/src/lib/mock-data/alert-data.ts +275 -0
  101. package/src/lib/mock-data/banking-data.ts +72 -0
  102. package/src/lib/mock-data/business-data.ts +71 -0
  103. package/src/lib/mock-data/counterparty-data.ts +70 -0
  104. package/src/lib/mock-data/index.ts +5 -0
  105. package/src/lib/mock-data/transaction-data.ts +283 -0
  106. package/src/lib/mock-data/wire-data.ts +103 -0
  107. package/src/lib/mock-data.tsx +180 -0
  108. package/src/lib/schemas/banking-schemas.ts +30 -0
  109. package/src/lib/schemas/business-schemas.ts +36 -0
  110. package/src/lib/schemas/counterparty-schemas.ts +43 -0
  111. package/src/lib/schemas/index.ts +5 -0
  112. package/src/lib/schemas/wire-schemas.ts +44 -0
  113. package/src/lib/utils.ts +6 -0
  114. package/src/main.tsx +10 -0
  115. package/src/pages/Cases.tsx +16 -0
  116. package/src/pages/Dashboard.tsx +16 -0
  117. package/src/pages/NotFound.tsx +27 -0
  118. package/src/pages/TransactionHistory.tsx +532 -0
  119. package/src/pages/UIKit.tsx +51 -0
  120. package/src/pages/alerts/AlertDetail.tsx +193 -0
  121. package/src/pages/alerts/Alerts.tsx +373 -0
  122. package/src/pages/business/Business.tsx +48 -0
  123. package/src/pages/business/Create.tsx +173 -0
  124. package/src/pages/counterparty/Create.tsx +48 -0
  125. package/src/pages/counterparty/DomesticWire.tsx +78 -0
  126. package/src/pages/counterparty/Manage.tsx +79 -0
  127. package/src/pages/transactions/NewTransaction.tsx +527 -0
  128. package/src/pages/transactions/TransactionDetail.tsx +192 -0
  129. package/src/vite-env.d.ts +1 -0
  130. package/tailwind.config.ts +124 -0
  131. package/tsconfig.app.json +30 -0
  132. package/tsconfig.json +19 -0
  133. package/tsconfig.node.json +22 -0
  134. package/vite.config.ts +22 -0
  135. package/dist/css/braid-ui-variables.css +0 -88
  136. package/dist/css/braid-ui.css +0 -4484
  137. package/dist/css/braid-ui.min.css +0 -1
  138. package/dist/index.cjs +0 -4
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.d.cts +0 -2429
  141. package/dist/index.d.ts +0 -2429
  142. package/dist/index.js +0 -4
  143. package/dist/index.js.map +0 -1
  144. package/src/styles-only.css +0 -121
  145. /package/{dist/braid-logo-343BOQZ2.png → src/assets/braid-logo.png} +0 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
@@ -0,0 +1,14 @@
1
+ User-agent: Googlebot
2
+ Allow: /
3
+
4
+ User-agent: Bingbot
5
+ Allow: /
6
+
7
+ User-agent: Twitterbot
8
+ Allow: /
9
+
10
+ User-agent: facebookexternalhit
11
+ Allow: /
12
+
13
+ User-agent: *
14
+ Allow: /
package/src/App.css ADDED
@@ -0,0 +1,42 @@
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,94 @@
1
+ import { Toaster } from "@/components/ui/toaster";
2
+ import { Toaster as Sonner } from "@/components/ui/sonner";
3
+ import { TooltipProvider } from "@/components/ui/tooltip";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
6
+ import { SidebarProvider } from "@/components/ui/sidebar";
7
+ import { AppSidebar } from "@/components/app-sidebar";
8
+ import { MainLayout } from "@/components/MainLayout";
9
+ import NotFound from "./pages/NotFound";
10
+
11
+ // Counterparty pages
12
+ import CounterpartyManage from "./pages/counterparty/Manage";
13
+ import CreateCounterparty from "./pages/counterparty/Create";
14
+ import CounterpartyDomesticWire from "./pages/counterparty/DomesticWire";
15
+
16
+ // Alerts pages
17
+ import Alerts from "./pages/alerts/Alerts";
18
+ import AlertDetail from "./pages/alerts/AlertDetail";
19
+
20
+ // Dashboard page
21
+ import Dashboard from "./pages/Dashboard";
22
+
23
+ // Cases page
24
+ import Cases from "./pages/Cases";
25
+
26
+ // Business pages
27
+ import Business from "./pages/business/Business";
28
+ import CreateBusiness from "./pages/business/Create";
29
+
30
+ // UI Kit pages
31
+ import UIKit from "./pages/UIKit";
32
+
33
+ // Transaction pages
34
+ import TransactionHistory from "./pages/TransactionHistory";
35
+ import TransactionDetail from "./pages/transactions/TransactionDetail";
36
+ import NewTransaction from "./pages/transactions/NewTransaction";
37
+
38
+ const queryClient = new QueryClient();
39
+
40
+ const App = () => (
41
+ <QueryClientProvider client={queryClient}>
42
+ <TooltipProvider>
43
+ <Toaster />
44
+ <Sonner />
45
+ <BrowserRouter>
46
+ <SidebarProvider>
47
+ <div className="flex min-h-screen w-full">
48
+ <AppSidebar />
49
+ <MainLayout>
50
+ <Routes>
51
+ <Route path="/" element={<Navigate to="/dashboard" replace />} />
52
+
53
+ {/* Dashboard Route */}
54
+ <Route path="/dashboard" element={<Dashboard />} />
55
+
56
+ {/* Alerts Routes */}
57
+ <Route path="/alerts" element={<Alerts />} />
58
+ <Route path="/alerts/:id" element={<AlertDetail />} />
59
+ <Route path="/cases" element={<Cases />} />
60
+
61
+ {/* Counterparty Routes */}
62
+ <Route path="/counterparty/manage" element={<CounterpartyManage />} />
63
+ <Route path="/counterparty/create" element={<CreateCounterparty />} />
64
+ <Route path="/counterparty/domestic-wire" element={<CounterpartyDomesticWire />} />
65
+
66
+ {/* Legacy redirects */}
67
+ <Route path="/counterparty/details" element={<Navigate to="/counterparty/manage" replace />} />
68
+ <Route path="/counterparty/ach" element={<Navigate to="/counterparty/manage" replace />} />
69
+ <Route path="/counterparty/wire" element={<Navigate to="/counterparty/manage" replace />} />
70
+
71
+ {/* Business Routes */}
72
+ <Route path="/business" element={<Business />} />
73
+ <Route path="/business/create" element={<CreateBusiness />} />
74
+
75
+ {/* UI Kit Routes */}
76
+ <Route path="/ui-kit" element={<UIKit />} />
77
+
78
+ {/* Transaction Routes */}
79
+ <Route path="/transactions/history" element={<TransactionHistory />} />
80
+ <Route path="/transactions/new" element={<NewTransaction />} />
81
+ <Route path="/transactions/:id" element={<TransactionDetail />} />
82
+
83
+ {/* Catch-all route */}
84
+ <Route path="*" element={<NotFound />} />
85
+ </Routes>
86
+ </MainLayout>
87
+ </div>
88
+ </SidebarProvider>
89
+ </BrowserRouter>
90
+ </TooltipProvider>
91
+ </QueryClientProvider>
92
+ );
93
+
94
+ export default App;
@@ -0,0 +1,15 @@
1
+ import { ReactNode } from "react"
2
+
3
+ interface MainLayoutProps {
4
+ children: ReactNode
5
+ }
6
+
7
+ export const MainLayout = ({ children }: MainLayoutProps) => {
8
+ return (
9
+ <div className="flex min-h-screen w-full">
10
+ <main className="flex-1 pl-4">
11
+ {children}
12
+ </main>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,320 @@
1
+ import { useState } from "react"
2
+ import { createPortal } from "react-dom"
3
+ import { AlertDocument } from "@/lib/mock-data/alert-data"
4
+ import { Button } from "@/components/ui/button"
5
+ import { toast } from "@/hooks/use-toast"
6
+ import { FileText, Upload, Download, Eye, Trash2, X } from "lucide-react"
7
+ import { cn } from "@/lib/utils"
8
+ import { EnhancedInput } from "@/components/ui/enhanced-input"
9
+ import { EnhancedTextarea } from "@/components/ui/enhanced-textarea"
10
+ import { EnhancedSelect } from "@/components/ui/enhanced-select"
11
+
12
+ interface AlertDocumentsProps {
13
+ alertId: string
14
+ documents: AlertDocument[]
15
+ }
16
+
17
+ export const AlertDocuments = ({ alertId, documents }: AlertDocumentsProps) => {
18
+ const [isUploading, setIsUploading] = useState(false)
19
+ const [showUploadDialog, setShowUploadDialog] = useState(false)
20
+ const [selectedFile, setSelectedFile] = useState<File | null>(null)
21
+ const [documentName, setDocumentName] = useState("")
22
+ const [description, setDescription] = useState("")
23
+ const [documentType, setDocumentType] = useState("")
24
+
25
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
26
+ const file = e.target.files?.[0]
27
+ if (!file) return
28
+
29
+ setSelectedFile(file)
30
+ setDocumentName(file.name)
31
+ }
32
+
33
+ const handleUploadSubmit = () => {
34
+ if (!selectedFile || !documentName || !documentType) {
35
+ toast({
36
+ title: "Missing Information",
37
+ description: "Please fill in all required fields",
38
+ variant: "destructive"
39
+ })
40
+ return
41
+ }
42
+
43
+ setIsUploading(true)
44
+
45
+ // Simulate upload
46
+ setTimeout(() => {
47
+ const newDocument: AlertDocument = {
48
+ id: String(Date.now()),
49
+ name: documentName,
50
+ type: documentType,
51
+ description: description || undefined,
52
+ size: `${(selectedFile.size / 1024 / 1024).toFixed(1)} MB`,
53
+ uploadedBy: "Current User",
54
+ uploadedAt: new Date().toISOString()
55
+ }
56
+
57
+ toast({
58
+ title: "Upload Successful",
59
+ description: `${documentName} uploaded successfully`
60
+ })
61
+ setIsUploading(false)
62
+ setShowUploadDialog(false)
63
+ resetForm()
64
+ }, 1000)
65
+ }
66
+
67
+ const resetForm = () => {
68
+ setSelectedFile(null)
69
+ setDocumentName("")
70
+ setDescription("")
71
+ setDocumentType("")
72
+ const fileInput = document.getElementById("file-upload") as HTMLInputElement
73
+ if (fileInput) fileInput.value = ""
74
+ }
75
+
76
+ const handleCancelUpload = () => {
77
+ setShowUploadDialog(false)
78
+ resetForm()
79
+ }
80
+
81
+ const isImageType = (name: string) => {
82
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp']
83
+ return imageExtensions.some(ext => name.toLowerCase().endsWith(ext))
84
+ }
85
+
86
+ const getFileIcon = (type: string) => {
87
+ return <FileText className="h-5 w-5" />
88
+ }
89
+
90
+ const getFileTypeColor = (type: string) => {
91
+ switch (type.toUpperCase()) {
92
+ case "PDF":
93
+ return "text-red-500"
94
+ case "DOCX":
95
+ case "DOC":
96
+ return "text-blue-500"
97
+ case "XLSX":
98
+ case "XLS":
99
+ return "text-green-500"
100
+ case "JPG":
101
+ case "JPEG":
102
+ case "PNG":
103
+ return "text-purple-500"
104
+ default:
105
+ return "text-muted-foreground"
106
+ }
107
+ }
108
+
109
+ const documentTypeOptions = [
110
+ { value: "ID_DOCUMENT_FRONT", label: "ID Document (Front)" },
111
+ { value: "ID_DOCUMENT_BACK", label: "ID Document (Back)" },
112
+ { value: "PASSPORT", label: "Passport" },
113
+ { value: "PROOF_OF_ADDRESS", label: "Proof of Address" },
114
+ { value: "BANK_STATEMENT", label: "Bank Statement" },
115
+ { value: "BUSINESS_LICENSE", label: "Business License" },
116
+ { value: "TAX_DOCUMENT", label: "Tax Document" },
117
+ { value: "OTHER", label: "Other" }
118
+ ]
119
+
120
+ return (
121
+ <div className="space-y-6">
122
+ {/* Upload Dialog - Render using portal */}
123
+ {showUploadDialog && createPortal(
124
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
125
+ <div
126
+ className="fixed inset-0 bg-background/80 backdrop-blur-sm"
127
+ onClick={handleCancelUpload}
128
+ />
129
+ <div className="relative bg-card border rounded-lg shadow-lg max-w-lg w-full p-6 z-[101]">
130
+ <div className="flex items-center justify-between mb-6">
131
+ <h2 className="text-2xl font-bold">Upload Document</h2>
132
+ <Button
133
+ variant="ghost"
134
+ size="sm"
135
+ className="h-8 w-8 p-0"
136
+ onClick={handleCancelUpload}
137
+ >
138
+ <X className="h-4 w-4" />
139
+ </Button>
140
+ </div>
141
+
142
+ <div className="space-y-4">
143
+ {/* File Upload Area Inside Dialog */}
144
+ <div className="border-2 border-dashed rounded-lg p-6 text-center hover:border-primary/50 transition-colors">
145
+ <input
146
+ type="file"
147
+ id="file-upload-dialog"
148
+ className="hidden"
149
+ onChange={handleFileSelect}
150
+ disabled={isUploading}
151
+ />
152
+ <label
153
+ htmlFor="file-upload-dialog"
154
+ className={cn(
155
+ "cursor-pointer flex flex-col items-center gap-2",
156
+ isUploading && "opacity-50 cursor-not-allowed"
157
+ )}
158
+ >
159
+ <Upload className="h-8 w-8 text-muted-foreground" />
160
+ <div>
161
+ <p className="text-sm font-medium text-foreground">
162
+ {selectedFile ? "Change file" : "Click to upload or drag and drop"}
163
+ </p>
164
+ <p className="text-xs text-muted-foreground mt-1">
165
+ PDF, DOC, DOCX, JPG, PNG up to 10MB
166
+ </p>
167
+ </div>
168
+ </label>
169
+ </div>
170
+
171
+ {selectedFile && (
172
+ <div className="p-3 bg-muted/50 rounded-lg">
173
+ <p className="text-sm text-muted-foreground">Selected file:</p>
174
+ <p className="text-sm font-medium">{selectedFile.name}</p>
175
+ <p className="text-xs text-muted-foreground">
176
+ {(selectedFile.size / 1024 / 1024).toFixed(2)} MB
177
+ </p>
178
+ </div>
179
+ )}
180
+
181
+ <EnhancedInput
182
+ label="Document name"
183
+ value={documentName}
184
+ onChange={(e) => setDocumentName(e.target.value)}
185
+ placeholder="Enter document name"
186
+ />
187
+
188
+ <EnhancedTextarea
189
+ label="Description"
190
+ value={description}
191
+ onChange={(e) => setDescription(e.target.value)}
192
+ placeholder="Enter document description (optional)"
193
+ rows={3}
194
+ />
195
+
196
+ <EnhancedSelect
197
+ label="Document type"
198
+ value={documentType}
199
+ onValueChange={setDocumentType}
200
+ placeholder="Select document type"
201
+ options={documentTypeOptions}
202
+ />
203
+ </div>
204
+
205
+ <div className="flex gap-3 mt-6">
206
+ <Button
207
+ variant="outline"
208
+ onClick={handleCancelUpload}
209
+ disabled={isUploading}
210
+ className="flex-1"
211
+ >
212
+ Cancel
213
+ </Button>
214
+ <Button
215
+ onClick={handleUploadSubmit}
216
+ disabled={isUploading}
217
+ className="flex-1"
218
+ >
219
+ {isUploading ? "Uploading..." : "Upload"}
220
+ </Button>
221
+ </div>
222
+ </div>
223
+ </div>,
224
+ document.body
225
+ )}
226
+
227
+ {/* Documents List */}
228
+ <div className="space-y-3">
229
+ <div className="flex items-center justify-between">
230
+ <h3 className="text-sm font-medium text-foreground">Uploaded Documents</h3>
231
+ <Button
232
+ onClick={() => setShowUploadDialog(true)}
233
+ variant="ghost"
234
+ size="sm"
235
+ className="gap-2 text-primary hover:text-primary"
236
+ >
237
+ <Upload className="h-4 w-4" />
238
+ Upload
239
+ </Button>
240
+ </div>
241
+ {documents.length === 0 ? (
242
+ <div className="text-center py-8 text-muted-foreground">
243
+ <FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
244
+ <p>No documents uploaded yet</p>
245
+ </div>
246
+ ) : (
247
+ <div className="space-y-2">
248
+ {documents.map((doc) => (
249
+ <div
250
+ key={doc.id}
251
+ className="border rounded-lg hover:bg-muted/30 transition-colors overflow-hidden"
252
+ >
253
+ {/* Image Preview for image types */}
254
+ {isImageType(doc.name) && doc.url && (
255
+ <div className="w-full bg-muted/50">
256
+ <img
257
+ src={doc.url}
258
+ alt={doc.name}
259
+ className="w-full h-48 object-cover"
260
+ />
261
+ </div>
262
+ )}
263
+
264
+ <div className="flex items-center justify-between p-3">
265
+ <div className="flex items-center gap-3 flex-1 min-w-0">
266
+ {!isImageType(doc.name) && (
267
+ <div className={cn(getFileTypeColor(doc.type))}>
268
+ {getFileIcon(doc.type)}
269
+ </div>
270
+ )}
271
+ <div className="flex-1 min-w-0">
272
+ <div className="flex items-center gap-2">
273
+ <h4 className="font-medium text-sm truncate">{doc.name}</h4>
274
+ <span className={cn("text-xs px-2 py-0.5 rounded-full bg-muted", getFileTypeColor(doc.type))}>
275
+ {doc.type}
276
+ </span>
277
+ </div>
278
+ {doc.description && (
279
+ <p className="text-sm text-foreground mt-1">{doc.description}</p>
280
+ )}
281
+ <p className="text-xs text-muted-foreground mt-1">
282
+ {doc.size} • Uploaded by {doc.uploadedBy} • {new Date(doc.uploadedAt).toLocaleDateString()}
283
+ </p>
284
+ </div>
285
+ </div>
286
+ <div className="flex items-center gap-1">
287
+ <Button
288
+ variant="ghost"
289
+ size="sm"
290
+ className="h-8 w-8 p-0"
291
+ onClick={() => toast({ title: "Preview", description: `Viewing ${doc.name}` })}
292
+ >
293
+ <Eye className="h-4 w-4" />
294
+ </Button>
295
+ <Button
296
+ variant="ghost"
297
+ size="sm"
298
+ className="h-8 w-8 p-0"
299
+ onClick={() => toast({ title: "Download", description: `Downloading ${doc.name}` })}
300
+ >
301
+ <Download className="h-4 w-4" />
302
+ </Button>
303
+ <Button
304
+ variant="ghost"
305
+ size="sm"
306
+ className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
307
+ onClick={() => toast({ title: "Delete", description: `Deleted ${doc.name}` })}
308
+ >
309
+ <Trash2 className="h-4 w-4" />
310
+ </Button>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ )}
317
+ </div>
318
+ </div>
319
+ )
320
+ }
@@ -0,0 +1,185 @@
1
+ import { useState } from "react"
2
+ import { createPortal } from "react-dom"
3
+ import { AlertNote } from "@/lib/mock-data/alert-data"
4
+ import { Button } from "@/components/ui/button"
5
+ import { EnhancedTextarea } from "@/components/ui/enhanced-textarea"
6
+ import { EnhancedSelect } from "@/components/ui/enhanced-select"
7
+ import { Badge } from "@/components/ui/badge"
8
+ import { toast } from "@/hooks/use-toast"
9
+ import { MessageSquare, Plus, X, Trash2 } from "lucide-react"
10
+
11
+ const NOTE_TYPE_OPTIONS = [
12
+ { value: "RFI Note", label: "RFI Note" },
13
+ { value: "Internal Note", label: "Internal Note" }
14
+ ]
15
+
16
+ interface AlertNotesProps {
17
+ alertId: string
18
+ notes: AlertNote[]
19
+ }
20
+
21
+ export const AlertNotes = ({ alertId, notes }: AlertNotesProps) => {
22
+ const [showNoteDialog, setShowNoteDialog] = useState(false)
23
+ const [newNote, setNewNote] = useState("")
24
+ const [noteType, setNoteType] = useState<"RFI Note" | "Internal Note">("RFI Note")
25
+ const [isSubmitting, setIsSubmitting] = useState(false)
26
+
27
+ const resetForm = () => {
28
+ setNewNote("")
29
+ setNoteType("RFI Note")
30
+ }
31
+
32
+ const handleCancelNote = () => {
33
+ setShowNoteDialog(false)
34
+ resetForm()
35
+ }
36
+
37
+ const handleAddNote = async () => {
38
+ if (!newNote.trim()) {
39
+ toast({
40
+ title: "Error",
41
+ description: "Please enter a note before submitting",
42
+ variant: "destructive"
43
+ })
44
+ return
45
+ }
46
+
47
+ setIsSubmitting(true)
48
+
49
+ // Simulate API call
50
+ setTimeout(() => {
51
+ toast({
52
+ title: "Note Added",
53
+ description: "Your note has been added successfully"
54
+ })
55
+ setShowNoteDialog(false)
56
+ resetForm()
57
+ setIsSubmitting(false)
58
+ }, 500)
59
+ }
60
+
61
+ const handleDeleteNote = (noteId: string, noteContent: string) => {
62
+ toast({
63
+ title: "Note Deleted",
64
+ description: `Note deleted successfully`
65
+ })
66
+ // TODO: API call to delete note
67
+ console.log("Deleting note:", noteId)
68
+ }
69
+
70
+ return (
71
+ <div className="space-y-6">
72
+ {/* Add Note Dialog - Render using portal */}
73
+ {showNoteDialog && createPortal(
74
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
75
+ <div
76
+ className="fixed inset-0 bg-background/80 backdrop-blur-sm"
77
+ onClick={handleCancelNote}
78
+ />
79
+ <div className="relative bg-card border rounded-lg shadow-lg max-w-lg w-full p-6 z-[101]">
80
+ <div className="flex items-center justify-between mb-6">
81
+ <h2 className="text-2xl font-bold">Add Note</h2>
82
+ <Button
83
+ variant="ghost"
84
+ size="sm"
85
+ className="h-8 w-8 p-0"
86
+ onClick={handleCancelNote}
87
+ >
88
+ <X className="h-4 w-4" />
89
+ </Button>
90
+ </div>
91
+
92
+ <div className="space-y-4">
93
+ <EnhancedSelect
94
+ label="Note Type"
95
+ value={noteType}
96
+ onValueChange={(value) => setNoteType(value as "RFI Note" | "Internal Note")}
97
+ options={NOTE_TYPE_OPTIONS}
98
+ />
99
+ <EnhancedTextarea
100
+ label="Note"
101
+ placeholder="Enter your note here..."
102
+ value={newNote}
103
+ onChange={(e) => setNewNote(e.target.value)}
104
+ rows={6}
105
+ />
106
+ </div>
107
+
108
+ <div className="flex gap-3 mt-6">
109
+ <Button
110
+ variant="outline"
111
+ onClick={handleCancelNote}
112
+ disabled={isSubmitting}
113
+ className="flex-1"
114
+ >
115
+ Cancel
116
+ </Button>
117
+ <Button
118
+ onClick={handleAddNote}
119
+ disabled={isSubmitting || !newNote.trim()}
120
+ className="flex-1"
121
+ >
122
+ {isSubmitting ? "Adding..." : "Add Note"}
123
+ </Button>
124
+ </div>
125
+ </div>
126
+ </div>,
127
+ document.body
128
+ )}
129
+
130
+ {/* Notes History */}
131
+ <div className="space-y-4">
132
+ <div className="flex items-center justify-between">
133
+ <h3 className="text-sm font-medium text-foreground">Note History</h3>
134
+ <Button
135
+ onClick={() => setShowNoteDialog(true)}
136
+ variant="ghost"
137
+ size="sm"
138
+ className="gap-2 text-primary hover:text-primary"
139
+ >
140
+ <Plus className="h-4 w-4" />
141
+ Add Note
142
+ </Button>
143
+ </div>
144
+ {notes.length === 0 ? (
145
+ <div className="text-center py-8 text-muted-foreground">
146
+ <MessageSquare className="h-8 w-8 mx-auto mb-2 opacity-50" />
147
+ <p>No notes yet</p>
148
+ </div>
149
+ ) : (
150
+ <div className="space-y-3">
151
+ {notes.map((note) => (
152
+ <div
153
+ key={note.id}
154
+ className="border rounded-lg p-4 bg-muted/30"
155
+ >
156
+ <div className="flex items-start justify-between gap-4 mb-2">
157
+ <div className="flex items-center gap-2">
158
+ <p className="font-medium text-sm text-foreground">{note.user}</p>
159
+ <Badge variant={note.type === "RFI Note" ? "default" : "outline"}>
160
+ {note.type}
161
+ </Badge>
162
+ </div>
163
+ <div className="flex items-center gap-2">
164
+ <span className="text-xs text-muted-foreground whitespace-nowrap">
165
+ {note.timestamp}
166
+ </span>
167
+ <Button
168
+ variant="ghost"
169
+ size="sm"
170
+ className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
171
+ onClick={() => handleDeleteNote(note.id, note.content)}
172
+ >
173
+ <Trash2 className="h-4 w-4" />
174
+ </Button>
175
+ </div>
176
+ </div>
177
+ <p className="text-sm text-foreground">{note.content}</p>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ )}
182
+ </div>
183
+ </div>
184
+ )
185
+ }