@tidecloak/ui-framework 0.0.1
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 +377 -0
- package/dist/index.d.mts +2739 -0
- package/dist/index.d.ts +2739 -0
- package/dist/index.js +12869 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12703 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/src/components/common/ActionButton.tsx +234 -0
- package/src/components/common/EmptyState.tsx +140 -0
- package/src/components/common/LoadingSkeleton.tsx +121 -0
- package/src/components/common/RefreshButton.tsx +127 -0
- package/src/components/common/StatusBadge.tsx +177 -0
- package/src/components/common/index.ts +31 -0
- package/src/components/data-table/DataTable.tsx +201 -0
- package/src/components/data-table/PaginatedTable.tsx +247 -0
- package/src/components/data-table/index.ts +2 -0
- package/src/components/dialogs/CollapsibleSection.tsx +184 -0
- package/src/components/dialogs/ConfirmDialog.tsx +264 -0
- package/src/components/dialogs/DetailDialog.tsx +228 -0
- package/src/components/dialogs/index.ts +3 -0
- package/src/components/index.ts +5 -0
- package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
- package/src/components/pages/base/LogsPageBase.tsx +581 -0
- package/src/components/pages/base/RolesPageBase.tsx +1470 -0
- package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
- package/src/components/pages/base/UsersPageBase.tsx +843 -0
- package/src/components/pages/base/index.ts +58 -0
- package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
- package/src/components/pages/connected/LogsPage.tsx +267 -0
- package/src/components/pages/connected/RolesPage.tsx +525 -0
- package/src/components/pages/connected/TemplatesPage.tsx +181 -0
- package/src/components/pages/connected/UsersPage.tsx +237 -0
- package/src/components/pages/connected/index.ts +36 -0
- package/src/components/pages/index.ts +5 -0
- package/src/components/tabs/TabsView.tsx +300 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/ui/index.tsx +1001 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAutoRefresh.ts +119 -0
- package/src/hooks/usePagination.ts +152 -0
- package/src/hooks/useSelection.ts +81 -0
- package/src/index.ts +256 -0
- package/src/theme.ts +185 -0
- package/src/tide/index.ts +19 -0
- package/src/tide/tidePolicy.ts +270 -0
- package/src/types/index.ts +484 -0
- package/src/utils/index.ts +121 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
3
|
+
import { cn } from "../../utils";
|
|
4
|
+
|
|
5
|
+
export interface CollapsibleSectionProps {
|
|
6
|
+
/** Trigger/header content */
|
|
7
|
+
trigger: React.ReactNode;
|
|
8
|
+
/** Collapsible content */
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/** Default open state */
|
|
11
|
+
defaultOpen?: boolean;
|
|
12
|
+
/** Controlled open state */
|
|
13
|
+
open?: boolean;
|
|
14
|
+
/** Open state change handler */
|
|
15
|
+
onOpenChange?: (open: boolean) => void;
|
|
16
|
+
/** Icon to show when collapsed */
|
|
17
|
+
collapsedIcon?: React.ReactNode;
|
|
18
|
+
/** Icon to show when expanded */
|
|
19
|
+
expandedIcon?: React.ReactNode;
|
|
20
|
+
/** Additional class name */
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Trigger class name */
|
|
23
|
+
triggerClassName?: string;
|
|
24
|
+
/** Content class name */
|
|
25
|
+
contentClassName?: string;
|
|
26
|
+
/** Custom components */
|
|
27
|
+
components?: {
|
|
28
|
+
Collapsible?: React.ComponentType<{
|
|
29
|
+
open: boolean;
|
|
30
|
+
onOpenChange: (open: boolean) => void;
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
}>;
|
|
33
|
+
CollapsibleTrigger?: React.ComponentType<{
|
|
34
|
+
asChild?: boolean;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}>;
|
|
37
|
+
CollapsibleContent?: React.ComponentType<{
|
|
38
|
+
children: React.ReactNode;
|
|
39
|
+
className?: string;
|
|
40
|
+
}>;
|
|
41
|
+
Button?: React.ComponentType<{
|
|
42
|
+
variant?: string;
|
|
43
|
+
size?: string;
|
|
44
|
+
className?: string;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
onClick?: () => void;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* CollapsibleSection - Expandable content section
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <CollapsibleSection
|
|
57
|
+
* trigger={<><Code className="h-4 w-4" /> View Code</>}
|
|
58
|
+
* defaultOpen={false}
|
|
59
|
+
* >
|
|
60
|
+
* <pre className="p-3 text-xs font-mono">{code}</pre>
|
|
61
|
+
* </CollapsibleSection>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function CollapsibleSection({
|
|
65
|
+
trigger,
|
|
66
|
+
children,
|
|
67
|
+
defaultOpen = false,
|
|
68
|
+
open: controlledOpen,
|
|
69
|
+
onOpenChange,
|
|
70
|
+
collapsedIcon,
|
|
71
|
+
expandedIcon,
|
|
72
|
+
className,
|
|
73
|
+
triggerClassName,
|
|
74
|
+
contentClassName,
|
|
75
|
+
components = {},
|
|
76
|
+
}: CollapsibleSectionProps) {
|
|
77
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
78
|
+
|
|
79
|
+
const isControlled = controlledOpen !== undefined;
|
|
80
|
+
const isOpen = isControlled ? controlledOpen : internalOpen;
|
|
81
|
+
|
|
82
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
83
|
+
if (!isControlled) {
|
|
84
|
+
setInternalOpen(newOpen);
|
|
85
|
+
}
|
|
86
|
+
onOpenChange?.(newOpen);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Custom components
|
|
90
|
+
const Collapsible = components.Collapsible;
|
|
91
|
+
const CollapsibleTrigger = components.CollapsibleTrigger;
|
|
92
|
+
const CollapsibleContent = components.CollapsibleContent;
|
|
93
|
+
const Button = components.Button;
|
|
94
|
+
|
|
95
|
+
// Use custom Radix-style components if all are provided
|
|
96
|
+
if (Collapsible && CollapsibleTrigger && CollapsibleContent && Button) {
|
|
97
|
+
return (
|
|
98
|
+
<Collapsible open={isOpen} onOpenChange={handleOpenChange}>
|
|
99
|
+
<CollapsibleTrigger asChild>
|
|
100
|
+
<Button
|
|
101
|
+
variant="ghost"
|
|
102
|
+
size="sm"
|
|
103
|
+
className={cn("w-full justify-start gap-2 text-muted-foreground hover:text-foreground", triggerClassName)}
|
|
104
|
+
>
|
|
105
|
+
{isOpen
|
|
106
|
+
? (expandedIcon || <ChevronDown className="h-4 w-4" />)
|
|
107
|
+
: (collapsedIcon || <ChevronRight className="h-4 w-4" />)
|
|
108
|
+
}
|
|
109
|
+
{trigger}
|
|
110
|
+
</Button>
|
|
111
|
+
</CollapsibleTrigger>
|
|
112
|
+
<CollapsibleContent className={contentClassName}>
|
|
113
|
+
{children}
|
|
114
|
+
</CollapsibleContent>
|
|
115
|
+
</Collapsible>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Default implementation
|
|
120
|
+
return (
|
|
121
|
+
<div className={className}>
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onClick={() => handleOpenChange(!isOpen)}
|
|
125
|
+
className={cn(
|
|
126
|
+
"flex w-full items-center justify-start gap-2 rounded-md px-3 py-2 text-sm",
|
|
127
|
+
"text-muted-foreground hover:text-foreground hover:bg-accent",
|
|
128
|
+
triggerClassName
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{isOpen
|
|
132
|
+
? (expandedIcon || <ChevronDown className="h-4 w-4" />)
|
|
133
|
+
: (collapsedIcon || <ChevronRight className="h-4 w-4" />)
|
|
134
|
+
}
|
|
135
|
+
{trigger}
|
|
136
|
+
</button>
|
|
137
|
+
{isOpen && (
|
|
138
|
+
<div className={cn("mt-2", contentClassName)}>
|
|
139
|
+
{children}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* CodePreview - Pre-styled collapsible code section
|
|
148
|
+
*/
|
|
149
|
+
export function CodePreview({
|
|
150
|
+
code,
|
|
151
|
+
language = "text",
|
|
152
|
+
label = "View Code",
|
|
153
|
+
icon,
|
|
154
|
+
maxHeight = "300px",
|
|
155
|
+
className,
|
|
156
|
+
...props
|
|
157
|
+
}: {
|
|
158
|
+
code: string;
|
|
159
|
+
language?: string;
|
|
160
|
+
label?: string;
|
|
161
|
+
icon?: React.ReactNode;
|
|
162
|
+
maxHeight?: string;
|
|
163
|
+
className?: string;
|
|
164
|
+
} & Omit<CollapsibleSectionProps, 'trigger' | 'children'>) {
|
|
165
|
+
return (
|
|
166
|
+
<CollapsibleSection
|
|
167
|
+
trigger={
|
|
168
|
+
<>
|
|
169
|
+
{icon}
|
|
170
|
+
{label}
|
|
171
|
+
</>
|
|
172
|
+
}
|
|
173
|
+
contentClassName={cn("mt-2 border rounded-lg bg-muted/50", className)}
|
|
174
|
+
{...props}
|
|
175
|
+
>
|
|
176
|
+
<pre
|
|
177
|
+
className="p-3 text-xs font-mono overflow-x-auto overflow-y-auto whitespace-pre-wrap"
|
|
178
|
+
style={{ maxHeight }}
|
|
179
|
+
>
|
|
180
|
+
{code}
|
|
181
|
+
</pre>
|
|
182
|
+
</CollapsibleSection>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface ConfirmDialogProps {
|
|
4
|
+
/** Whether dialog is open */
|
|
5
|
+
open: boolean;
|
|
6
|
+
/** Close handler */
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Confirm handler */
|
|
9
|
+
onConfirm: () => void | Promise<void>;
|
|
10
|
+
/** Dialog title */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Dialog description */
|
|
13
|
+
description: string;
|
|
14
|
+
/** Confirm button label */
|
|
15
|
+
confirmLabel?: string;
|
|
16
|
+
/** Cancel button label */
|
|
17
|
+
cancelLabel?: string;
|
|
18
|
+
/** Confirm button variant */
|
|
19
|
+
confirmVariant?: "default" | "destructive";
|
|
20
|
+
/** Whether action is in progress */
|
|
21
|
+
isLoading?: boolean;
|
|
22
|
+
/** Additional class name */
|
|
23
|
+
className?: string;
|
|
24
|
+
/** Custom components */
|
|
25
|
+
components?: {
|
|
26
|
+
AlertDialog?: React.ComponentType<{
|
|
27
|
+
open: boolean;
|
|
28
|
+
onOpenChange: (open: boolean) => void;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}>;
|
|
31
|
+
AlertDialogContent?: React.ComponentType<{ children: React.ReactNode }>;
|
|
32
|
+
AlertDialogHeader?: React.ComponentType<{ children: React.ReactNode }>;
|
|
33
|
+
AlertDialogTitle?: React.ComponentType<{ children: React.ReactNode }>;
|
|
34
|
+
AlertDialogDescription?: React.ComponentType<{ children: React.ReactNode }>;
|
|
35
|
+
AlertDialogFooter?: React.ComponentType<{ children: React.ReactNode }>;
|
|
36
|
+
AlertDialogAction?: React.ComponentType<{
|
|
37
|
+
onClick: () => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
style?: React.CSSProperties;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
}>;
|
|
43
|
+
AlertDialogCancel?: React.ComponentType<{
|
|
44
|
+
onClick?: () => void;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ConfirmDialog - Confirmation dialog for destructive actions
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <ConfirmDialog
|
|
56
|
+
* open={showDelete}
|
|
57
|
+
* onClose={() => setShowDelete(false)}
|
|
58
|
+
* onConfirm={handleDelete}
|
|
59
|
+
* title="Delete Item"
|
|
60
|
+
* description="Are you sure? This action cannot be undone."
|
|
61
|
+
* confirmLabel="Delete"
|
|
62
|
+
* confirmVariant="destructive"
|
|
63
|
+
* isLoading={isDeleting}
|
|
64
|
+
* />
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function ConfirmDialog({
|
|
68
|
+
open,
|
|
69
|
+
onClose,
|
|
70
|
+
onConfirm,
|
|
71
|
+
title,
|
|
72
|
+
description,
|
|
73
|
+
confirmLabel = "Confirm",
|
|
74
|
+
cancelLabel = "Cancel",
|
|
75
|
+
confirmVariant = "default",
|
|
76
|
+
isLoading = false,
|
|
77
|
+
className,
|
|
78
|
+
components = {},
|
|
79
|
+
}: ConfirmDialogProps) {
|
|
80
|
+
const AlertDialog = components.AlertDialog || DefaultAlertDialog;
|
|
81
|
+
const AlertDialogContent = components.AlertDialogContent || DefaultAlertDialogContent;
|
|
82
|
+
const AlertDialogHeader = components.AlertDialogHeader || DefaultAlertDialogHeader;
|
|
83
|
+
const AlertDialogTitle = components.AlertDialogTitle || DefaultAlertDialogTitle;
|
|
84
|
+
const AlertDialogDescription = components.AlertDialogDescription || DefaultAlertDialogDescription;
|
|
85
|
+
const AlertDialogFooter = components.AlertDialogFooter || DefaultAlertDialogFooter;
|
|
86
|
+
const AlertDialogAction = components.AlertDialogAction || DefaultAlertDialogAction;
|
|
87
|
+
const AlertDialogCancel = components.AlertDialogCancel || DefaultAlertDialogCancel;
|
|
88
|
+
|
|
89
|
+
const handleConfirm = async () => {
|
|
90
|
+
await onConfirm();
|
|
91
|
+
onClose();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const destructiveStyle: React.CSSProperties = confirmVariant === "destructive"
|
|
95
|
+
? { backgroundColor: "#ef4444", color: "#ffffff" }
|
|
96
|
+
: {};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<AlertDialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
100
|
+
<AlertDialogContent>
|
|
101
|
+
<AlertDialogHeader>
|
|
102
|
+
<AlertDialogTitle>{title}</AlertDialogTitle>
|
|
103
|
+
<AlertDialogDescription>{description}</AlertDialogDescription>
|
|
104
|
+
</AlertDialogHeader>
|
|
105
|
+
<AlertDialogFooter>
|
|
106
|
+
<AlertDialogCancel onClick={onClose}>{cancelLabel}</AlertDialogCancel>
|
|
107
|
+
<AlertDialogAction
|
|
108
|
+
onClick={handleConfirm}
|
|
109
|
+
disabled={isLoading}
|
|
110
|
+
style={destructiveStyle}
|
|
111
|
+
>
|
|
112
|
+
{isLoading ? "Processing..." : confirmLabel}
|
|
113
|
+
</AlertDialogAction>
|
|
114
|
+
</AlertDialogFooter>
|
|
115
|
+
</AlertDialogContent>
|
|
116
|
+
</AlertDialog>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Default alert dialog components with inline styles
|
|
121
|
+
function DefaultAlertDialog({
|
|
122
|
+
open,
|
|
123
|
+
onOpenChange,
|
|
124
|
+
children,
|
|
125
|
+
}: {
|
|
126
|
+
open: boolean;
|
|
127
|
+
onOpenChange: (open: boolean) => void;
|
|
128
|
+
children: React.ReactNode;
|
|
129
|
+
}) {
|
|
130
|
+
if (!open) return null;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div style={{ position: "fixed", inset: 0, zIndex: 50 }} role="presentation">
|
|
134
|
+
<div
|
|
135
|
+
style={{ position: "fixed", inset: 0, backgroundColor: "rgba(0, 0, 0, 0.8)" }}
|
|
136
|
+
onClick={() => onOpenChange(false)}
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
/>
|
|
139
|
+
<div style={{ position: "fixed", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }}>
|
|
140
|
+
{children}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function DefaultAlertDialogContent({ children }: { children: React.ReactNode }) {
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
role="alertdialog"
|
|
150
|
+
aria-modal="true"
|
|
151
|
+
aria-labelledby="alert-dialog-title"
|
|
152
|
+
aria-describedby="alert-dialog-description"
|
|
153
|
+
style={{
|
|
154
|
+
position: "relative",
|
|
155
|
+
zIndex: 50,
|
|
156
|
+
display: "grid",
|
|
157
|
+
width: "100%",
|
|
158
|
+
maxWidth: "32rem",
|
|
159
|
+
gap: "1rem",
|
|
160
|
+
border: "1px solid #e5e7eb",
|
|
161
|
+
backgroundColor: "#ffffff",
|
|
162
|
+
padding: "1.5rem",
|
|
163
|
+
boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
|
|
164
|
+
borderRadius: "0.5rem",
|
|
165
|
+
}}
|
|
166
|
+
onClick={(e) => e.stopPropagation()}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function DefaultAlertDialogHeader({ children }: { children: React.ReactNode }) {
|
|
174
|
+
return <div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>{children}</div>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function DefaultAlertDialogTitle({ children }: { children: React.ReactNode }) {
|
|
178
|
+
return <h2 id="alert-dialog-title" style={{ fontSize: "1.125rem", fontWeight: 600, margin: 0 }}>{children}</h2>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function DefaultAlertDialogDescription({ children }: { children: React.ReactNode }) {
|
|
182
|
+
return <p id="alert-dialog-description" style={{ fontSize: "0.875rem", color: "#6b7280", margin: 0 }}>{children}</p>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function DefaultAlertDialogFooter({ children }: { children: React.ReactNode }) {
|
|
186
|
+
return (
|
|
187
|
+
<div style={{ display: "flex", justifyContent: "flex-end", gap: "0.5rem" }}>
|
|
188
|
+
{children}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function DefaultAlertDialogAction({
|
|
194
|
+
onClick,
|
|
195
|
+
className,
|
|
196
|
+
style,
|
|
197
|
+
disabled,
|
|
198
|
+
children,
|
|
199
|
+
}: {
|
|
200
|
+
onClick: () => void;
|
|
201
|
+
className?: string;
|
|
202
|
+
style?: React.CSSProperties;
|
|
203
|
+
disabled?: boolean;
|
|
204
|
+
children: React.ReactNode;
|
|
205
|
+
}) {
|
|
206
|
+
const baseStyle: React.CSSProperties = {
|
|
207
|
+
display: "inline-flex",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "center",
|
|
210
|
+
borderRadius: "0.375rem",
|
|
211
|
+
fontSize: "0.875rem",
|
|
212
|
+
fontWeight: 500,
|
|
213
|
+
height: "2.5rem",
|
|
214
|
+
padding: "0.5rem 1rem",
|
|
215
|
+
backgroundColor: "#3b82f6",
|
|
216
|
+
color: "#ffffff",
|
|
217
|
+
border: "none",
|
|
218
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
219
|
+
opacity: disabled ? 0.5 : 1,
|
|
220
|
+
...style,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={onClick}
|
|
227
|
+
disabled={disabled}
|
|
228
|
+
style={baseStyle}
|
|
229
|
+
className={className}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
</button>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function DefaultAlertDialogCancel({
|
|
237
|
+
onClick,
|
|
238
|
+
children,
|
|
239
|
+
}: {
|
|
240
|
+
onClick?: () => void;
|
|
241
|
+
children: React.ReactNode;
|
|
242
|
+
}) {
|
|
243
|
+
return (
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={onClick}
|
|
247
|
+
style={{
|
|
248
|
+
display: "inline-flex",
|
|
249
|
+
alignItems: "center",
|
|
250
|
+
justifyContent: "center",
|
|
251
|
+
borderRadius: "0.375rem",
|
|
252
|
+
fontSize: "0.875rem",
|
|
253
|
+
fontWeight: 500,
|
|
254
|
+
height: "2.5rem",
|
|
255
|
+
padding: "0.5rem 1rem",
|
|
256
|
+
border: "1px solid #e5e7eb",
|
|
257
|
+
backgroundColor: "#ffffff",
|
|
258
|
+
cursor: "pointer",
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
</button>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { cn } from "../../utils";
|
|
4
|
+
import type { DetailField, BaseDataItem } from "../../types";
|
|
5
|
+
|
|
6
|
+
export interface DetailDialogProps<T extends BaseDataItem> {
|
|
7
|
+
/** Whether dialog is open */
|
|
8
|
+
open: boolean;
|
|
9
|
+
/** Close handler */
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
/** Item to display */
|
|
12
|
+
item: T | null;
|
|
13
|
+
/** Dialog title */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Dialog description */
|
|
16
|
+
description?: string;
|
|
17
|
+
/** Field definitions */
|
|
18
|
+
fields: DetailField<T>[];
|
|
19
|
+
/** Footer content (actions) */
|
|
20
|
+
footer?: React.ReactNode;
|
|
21
|
+
/** Additional content after fields */
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
/** Dialog size */
|
|
24
|
+
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
25
|
+
/** Additional class name */
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Custom components */
|
|
28
|
+
components?: {
|
|
29
|
+
Dialog?: React.ComponentType<{
|
|
30
|
+
open: boolean;
|
|
31
|
+
onOpenChange: (open: boolean) => void;
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}>;
|
|
34
|
+
DialogContent?: React.ComponentType<{
|
|
35
|
+
className?: string;
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
}>;
|
|
38
|
+
DialogHeader?: React.ComponentType<{ children: React.ReactNode }>;
|
|
39
|
+
DialogTitle?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
|
|
40
|
+
DialogDescription?: React.ComponentType<{ children: React.ReactNode }>;
|
|
41
|
+
DialogFooter?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SIZE_CLASSES = {
|
|
46
|
+
sm: "max-w-sm",
|
|
47
|
+
md: "max-w-md",
|
|
48
|
+
lg: "max-w-lg",
|
|
49
|
+
xl: "max-w-xl",
|
|
50
|
+
full: "max-w-4xl",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* DetailDialog - Dialog for displaying item details
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <DetailDialog
|
|
59
|
+
* open={!!selectedItem}
|
|
60
|
+
* onClose={() => setSelectedItem(null)}
|
|
61
|
+
* item={selectedItem}
|
|
62
|
+
* title="Policy Details"
|
|
63
|
+
* fields={[
|
|
64
|
+
* { key: 'role', label: 'Role', render: (item) => item.roleId },
|
|
65
|
+
* { key: 'status', label: 'Status', render: (item) => <StatusBadge status={item.status} /> },
|
|
66
|
+
* ]}
|
|
67
|
+
* footer={<Button onClick={() => handleApprove()}>Approve</Button>}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function DetailDialog<T extends BaseDataItem>({
|
|
72
|
+
open,
|
|
73
|
+
onClose,
|
|
74
|
+
item,
|
|
75
|
+
title,
|
|
76
|
+
description,
|
|
77
|
+
fields,
|
|
78
|
+
footer,
|
|
79
|
+
children,
|
|
80
|
+
size = "lg",
|
|
81
|
+
className,
|
|
82
|
+
components = {},
|
|
83
|
+
}: DetailDialogProps<T>) {
|
|
84
|
+
// Use custom components or defaults
|
|
85
|
+
const Dialog = components.Dialog || DefaultDialog;
|
|
86
|
+
const DialogContent = components.DialogContent || DefaultDialogContent;
|
|
87
|
+
const DialogHeader = components.DialogHeader || DefaultDialogHeader;
|
|
88
|
+
const DialogTitle = components.DialogTitle || DefaultDialogTitle;
|
|
89
|
+
const DialogDescription = components.DialogDescription || DefaultDialogDescription;
|
|
90
|
+
const DialogFooter = components.DialogFooter || DefaultDialogFooter;
|
|
91
|
+
|
|
92
|
+
if (!item) return null;
|
|
93
|
+
|
|
94
|
+
// Filter visible fields
|
|
95
|
+
const visibleFields = fields.filter(
|
|
96
|
+
(field) => !field.hidden || !field.hidden(item)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
|
|
101
|
+
<DialogContent className={cn(SIZE_CLASSES[size], "max-h-[90vh] overflow-y-auto", className)}>
|
|
102
|
+
<DialogHeader>
|
|
103
|
+
<DialogTitle>{title}</DialogTitle>
|
|
104
|
+
{description && <DialogDescription>{description}</DialogDescription>}
|
|
105
|
+
</DialogHeader>
|
|
106
|
+
|
|
107
|
+
<div className="space-y-4">
|
|
108
|
+
{/* Fields Grid */}
|
|
109
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
|
110
|
+
{visibleFields.map((field) => (
|
|
111
|
+
<div
|
|
112
|
+
key={field.key}
|
|
113
|
+
className={field.fullWidth ? "col-span-2" : ""}
|
|
114
|
+
>
|
|
115
|
+
<p className="text-muted-foreground">{field.label}</p>
|
|
116
|
+
<div className="font-medium mt-1">{field.render(item)}</div>
|
|
117
|
+
</div>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Additional content */}
|
|
122
|
+
{children}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Footer */}
|
|
126
|
+
{footer && <DialogFooter>{footer}</DialogFooter>}
|
|
127
|
+
</DialogContent>
|
|
128
|
+
</Dialog>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Default dialog components
|
|
133
|
+
function DefaultDialog({
|
|
134
|
+
open,
|
|
135
|
+
onOpenChange,
|
|
136
|
+
children,
|
|
137
|
+
}: {
|
|
138
|
+
open: boolean;
|
|
139
|
+
onOpenChange: (open: boolean) => void;
|
|
140
|
+
children: React.ReactNode;
|
|
141
|
+
}) {
|
|
142
|
+
if (!open) return null;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="fixed inset-0 z-50">
|
|
146
|
+
{/* Backdrop */}
|
|
147
|
+
<div
|
|
148
|
+
className="fixed inset-0 bg-black/80"
|
|
149
|
+
onClick={() => onOpenChange(false)}
|
|
150
|
+
/>
|
|
151
|
+
{/* Content container */}
|
|
152
|
+
<div className="fixed inset-0 flex items-center justify-center p-4">
|
|
153
|
+
{children}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function DefaultDialogContent({
|
|
160
|
+
className,
|
|
161
|
+
children,
|
|
162
|
+
}: {
|
|
163
|
+
className?: string;
|
|
164
|
+
children: React.ReactNode;
|
|
165
|
+
}) {
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
className={cn(
|
|
169
|
+
"relative z-50 grid w-full gap-4 border bg-background p-6 shadow-lg",
|
|
170
|
+
"duration-200 sm:rounded-lg",
|
|
171
|
+
className
|
|
172
|
+
)}
|
|
173
|
+
onClick={(e) => e.stopPropagation()}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function DefaultDialogHeader({ children }: { children: React.ReactNode }) {
|
|
181
|
+
return (
|
|
182
|
+
<div className="flex flex-col space-y-1.5 text-center sm:text-left">
|
|
183
|
+
{children}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function DefaultDialogTitle({
|
|
189
|
+
children,
|
|
190
|
+
className,
|
|
191
|
+
}: {
|
|
192
|
+
children: React.ReactNode;
|
|
193
|
+
className?: string;
|
|
194
|
+
}) {
|
|
195
|
+
return (
|
|
196
|
+
<h2
|
|
197
|
+
className={cn(
|
|
198
|
+
"text-lg font-semibold leading-none tracking-tight",
|
|
199
|
+
className
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</h2>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function DefaultDialogDescription({ children }: { children: React.ReactNode }) {
|
|
208
|
+
return <p className="text-sm text-muted-foreground">{children}</p>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function DefaultDialogFooter({
|
|
212
|
+
children,
|
|
213
|
+
className,
|
|
214
|
+
}: {
|
|
215
|
+
children: React.ReactNode;
|
|
216
|
+
className?: string;
|
|
217
|
+
}) {
|
|
218
|
+
return (
|
|
219
|
+
<div
|
|
220
|
+
className={cn(
|
|
221
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
|
222
|
+
className
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{children}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|