24c7d68490
Every Dialog/Menu/Popover in the app renders its content via MuiPaper, which is deliberately kept flat (1px border, no shadow -- Paper is used too broadly, e.g. as a plain content divider, to safely restyle globally). That meant every modal (confirm/prompt, AddJobModal, EditJobDialog, JobDetailsDialog) and every dropdown/Select menu in the app was still rendering flat-bordered despite every other screen this session moving to the floating-shadow mockup look. Added targeted MuiDialog/MuiPopover/MuiMenu paper overrides -- these win on specificity over MuiPaper's own defaults without touching MuiPaper itself, so every dialog and dropdown in the app picks up the rounded floating-shadow treatment from this one change instead of patching each dialog file individually. Also added MuiChip (full pill radius, matching every status/skill pill in the mockups) and MuiTooltip (matching corner radius) overrides, and gave the toast Snackbar/Alert a consistent radius + weight.
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
|
|
import { Alert, Button, Snackbar } from "@mui/material";
|
|
|
|
type Severity = "success" | "info" | "warning" | "error";
|
|
|
|
type Toast = {
|
|
open: boolean;
|
|
message: string;
|
|
severity: Severity;
|
|
actionLabel?: string;
|
|
onAction?: () => void;
|
|
};
|
|
|
|
type ToastApi = {
|
|
toast: (
|
|
message: string,
|
|
severity?: Severity,
|
|
action?: { label: string; onClick: () => void },
|
|
) => void;
|
|
};
|
|
|
|
const ToastContext = createContext<ToastApi | null>(null);
|
|
|
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
const [t, setT] = useState<Toast>({ open: false, message: "", severity: "info" });
|
|
|
|
const toast = useCallback(
|
|
(message: string, severity: Severity = "info", action?: { label: string; onClick: () => void }) => {
|
|
setT({
|
|
open: true,
|
|
message,
|
|
severity,
|
|
actionLabel: action?.label,
|
|
onAction: action?.onClick,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const value = useMemo(() => ({ toast }), [toast]);
|
|
|
|
return (
|
|
<ToastContext.Provider value={value}>
|
|
{children}
|
|
<Snackbar
|
|
open={t.open}
|
|
autoHideDuration={3500}
|
|
onClose={() => setT((p) => ({ ...p, open: false }))}
|
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
>
|
|
<Alert
|
|
onClose={() => setT((p) => ({ ...p, open: false }))}
|
|
severity={t.severity}
|
|
variant="filled"
|
|
sx={{ maxWidth: 420, borderRadius: 3, fontWeight: 600 }}
|
|
action={
|
|
t.actionLabel && t.onAction ? (
|
|
<Button
|
|
color="inherit"
|
|
size="small"
|
|
onClick={() => {
|
|
setT((p) => ({ ...p, open: false }));
|
|
t.onAction?.();
|
|
}}
|
|
>
|
|
{t.actionLabel}
|
|
</Button>
|
|
) : undefined
|
|
}
|
|
>
|
|
{t.message}
|
|
</Alert>
|
|
</Snackbar>
|
|
</ToastContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useToast(): ToastApi {
|
|
const v = useContext(ToastContext);
|
|
if (!v) throw new Error("useToast must be used within ToastProvider");
|
|
return v;
|
|
}
|