finalized UI and website for 1.0 release
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
IconButton,
|
||||
Typography,
|
||||
Button
|
||||
} from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type AppDialogProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
maxWidth?: "xs" | "sm" | "md" | "lg" | "xl";
|
||||
actions?: ReactNode;
|
||||
submitLabel?: string;
|
||||
onSubmit?: () => void;
|
||||
isSubmitting?: boolean;
|
||||
};
|
||||
|
||||
export function AppDialog({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
children,
|
||||
maxWidth = "sm",
|
||||
actions,
|
||||
submitLabel = "Save",
|
||||
onSubmit,
|
||||
isSubmitting = false
|
||||
}: AppDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth={maxWidth}
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
elevation: 0,
|
||||
variant: "outlined"
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<IconButton onClick={onClose} size="small" sx={{ color: "text.secondary" }}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{children}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
{actions ? actions : (
|
||||
<>
|
||||
<Button onClick={onClose} color="inherit">
|
||||
Cancel
|
||||
</Button>
|
||||
{onSubmit && (
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
variant="contained"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
Box
|
||||
} from "@mui/material";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Column<T> = {
|
||||
id: string;
|
||||
label: string;
|
||||
align?: "left" | "right" | "center";
|
||||
width?: string | number;
|
||||
render?: (row: T) => ReactNode;
|
||||
};
|
||||
|
||||
type DataTableProps<T> = {
|
||||
columns: Column<T>[];
|
||||
data: T[];
|
||||
keyField: keyof T;
|
||||
emptyMessage?: string;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export function DataTable<T>({
|
||||
columns,
|
||||
data,
|
||||
keyField,
|
||||
emptyMessage = "No data available",
|
||||
loading = false
|
||||
}: DataTableProps<T>) {
|
||||
return (
|
||||
<TableContainer component={Card} variant="outlined">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableCell
|
||||
key={col.id}
|
||||
align={col.align || "left"}
|
||||
width={col.width}
|
||||
>
|
||||
{col.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.length === 0 && !loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} align="center" sx={{ py: 8 }}>
|
||||
<Typography color="text.secondary">{emptyMessage}</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.map((row) => (
|
||||
<TableRow key={String(row[keyField])}>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col.id} align={col.align || "left"}>
|
||||
{col.render ? col.render(row) : (row as any)[col.id]}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type PageHeaderProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
};
|
||||
|
||||
export function PageHeader({ title, description, action }: PageHeaderProps) {
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={2} sx={{ mb: 4 }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h4" color="text.primary">
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 600 }}>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
{action && (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={action.icon ?? <AddIcon />}
|
||||
onClick={action.onClick}
|
||||
color="primary"
|
||||
sx={{ fontWeight: 600 }}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
import { InputAdornment, TextField, TextFieldProps } from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
export function SearchField(props: TextFieldProps) {
|
||||
return (
|
||||
<TextField
|
||||
placeholder="Search..."
|
||||
variant="outlined"
|
||||
size="small"
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ color: "text.secondary" }} />
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
maxWidth: 400,
|
||||
"& .MuiOutlinedInput-root": {
|
||||
bgcolor: "background.paper",
|
||||
transition: "all 0.2s",
|
||||
"&:hover": {
|
||||
bgcolor: "action.hover"
|
||||
},
|
||||
"&.Mui-focused": {
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: "0 4px 20px rgba(0,0,0,0.2)"
|
||||
}
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
import { Box, Typography, ChipProps } from "@mui/material";
|
||||
|
||||
type StatusType = "active" | "inactive" | "error" | "warning";
|
||||
|
||||
type StatusChipProps = {
|
||||
status: StatusType;
|
||||
label?: string;
|
||||
sx?: any;
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<StatusType, { color: string; label: string }> = {
|
||||
active: { color: "#22c55e", label: "Active" }, // Green-500
|
||||
inactive: { color: "#71717a", label: "Paused" }, // Zinc-500
|
||||
error: { color: "#ef4444", label: "Error" }, // Red-500
|
||||
warning: { color: "#f59e0b", label: "Warning" } // Amber-500
|
||||
};
|
||||
|
||||
export function StatusChip({ status, label, sx }: StatusChipProps) {
|
||||
const config = STATUS_CONFIG[status];
|
||||
const displayLabel = label || config.label;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: "9999px",
|
||||
bgcolor: "rgba(255,255,255,0.03)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
...sx
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
bgcolor: config.color,
|
||||
boxShadow: `0 0 8px ${config.color}66`
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: "text.primary",
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
{displayLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user