New Interface (initial commit)

This commit is contained in:
coolcoala
2025-07-04 02:28:27 +03:00
parent 4435a5aee4
commit 686490ded1
121 changed files with 12852 additions and 13274 deletions

View File

@@ -1,68 +1,30 @@
import React, { useSyncExternalStore } from "react";
import { Snackbar, Alert, IconButton, Box } from "@mui/material";
import { CloseRounded } from "@mui/icons-material";
"use client";
import { Toaster, toast } from "sonner";
import { useEffect, useSyncExternalStore } from "react";
import {
subscribeNotices,
hideNotice,
getSnapshotNotices,
hideNotice,
subscribeNotices,
} from "@/services/noticeService";
export const NoticeManager: React.FC = () => {
export const NoticeManager = () => {
const currentNotices = useSyncExternalStore(
subscribeNotices,
getSnapshotNotices,
);
const handleClose = (id: number) => {
hideNotice(id);
};
useEffect(() => {
for (const notice of currentNotices) {
const toastId = toast(notice.message, {
id: notice.id,
duration: notice.duration,
onDismiss: (t) => {
hideNotice(t.id as number);
},
});
}
}, [currentNotices]);
return (
<Box
sx={{
position: "fixed",
top: "20px",
right: "20px",
zIndex: 1500,
display: "flex",
flexDirection: "column",
gap: "10px",
maxWidth: "360px",
}}
>
{currentNotices.map((notice) => (
<Snackbar
key={notice.id}
open={true}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
sx={{
position: "relative",
transform: "none",
top: "auto",
right: "auto",
bottom: "auto",
left: "auto",
width: "100%",
}}
>
<Alert
severity={notice.type}
variant="filled"
sx={{ width: "100%" }}
action={
<IconButton
size="small"
color="inherit"
onClick={() => handleClose(notice.id)}
>
<CloseRounded fontSize="inherit" />
</IconButton>
}
>
{notice.message}
</Alert>
</Snackbar>
))}
</Box>
);
return <Toaster />;
};

View File

@@ -1,15 +1,18 @@
import { ReactNode } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
type SxProps,
type Theme,
} from "@mui/material";
import { LoadingButton } from "@mui/lab";
import { useTranslation } from "react-i18next";
// --- Новые импорты ---
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react"; // Иконка для спиннера
// --- Интерфейсы ---
interface Props {
title: ReactNode;
open: boolean;
@@ -18,12 +21,12 @@ interface Props {
disableOk?: boolean;
disableCancel?: boolean;
disableFooter?: boolean;
contentSx?: SxProps<Theme>;
className?: string; // Замена для contentSx, чтобы передавать классы Tailwind
children?: ReactNode;
loading?: boolean;
onOk?: () => void;
onCancel?: () => void;
onClose?: () => void;
onClose?: () => void; // onOpenChange в shadcn/ui делает то же самое
}
export interface DialogRef {
@@ -38,37 +41,44 @@ export const BaseDialog: React.FC<Props> = (props) => {
children,
okBtn,
cancelBtn,
contentSx,
className,
disableCancel,
disableOk,
disableFooter,
loading,
onClose,
onCancel,
onOk,
} = props;
const { t } = useTranslation();
return (
<Dialog open={open} onClose={props.onClose}>
<DialogTitle>{title}</DialogTitle>
// Управляем состоянием через onOpenChange, которое вызывает onClose
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose?.()}>
<DialogContent className={className}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<DialogContent sx={contentSx}>{children}</DialogContent>
{children}
{!disableFooter && (
<DialogActions>
{!disableCancel && (
<Button variant="outlined" onClick={props.onCancel}>
{cancelBtn}
</Button>
)}
{!disableOk && (
<LoadingButton
loading={loading}
variant="contained"
onClick={props.onOk}
>
{okBtn}
</LoadingButton>
)}
</DialogActions>
)}
{!disableFooter && (
<DialogFooter>
{!disableCancel && (
<Button variant="outline" onClick={onCancel} disabled={loading}>
{cancelBtn || t("Cancel")}
</Button>
)}
{!disableOk && (
<Button disabled={loading || disableOk} onClick={onOk}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{okBtn || t("Confirm")}
</Button>
)}
</DialogFooter>
)}
</DialogContent>
</Dialog>
);
};

View File

@@ -1,10 +1,10 @@
import { alpha, Box, Typography } from "@mui/material";
import { InboxRounded } from "@mui/icons-material";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Inbox } from "lucide-react"; // 1. Импортируем иконку из lucide-react
interface Props {
text?: React.ReactNode;
extra?: React.ReactNode;
text?: ReactNode;
extra?: ReactNode;
}
export const BaseEmpty = (props: Props) => {
@@ -12,20 +12,15 @@ export const BaseEmpty = (props: Props) => {
const { t } = useTranslation();
return (
<Box
sx={({ palette }) => ({
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: alpha(palette.text.secondary, 0.75),
})}
>
<InboxRounded sx={{ fontSize: "4em" }} />
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
// 2. Заменяем Box на div и переводим sx в классы Tailwind
<div className="flex h-full w-full flex-col items-center justify-center space-y-4 text-muted-foreground/75">
{/* 3. Заменяем иконку MUI на lucide-react и задаем размер классами */}
<Inbox className="h-20 w-20" />
{/* 4. Заменяем Typography на p */}
<p className="text-xl">{t(`${text}`)}</p>
{extra}
</Box>
</div>
);
};

View File

@@ -1,16 +1,30 @@
import { ReactNode } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { useTranslation } from "react-i18next";
import { AlertTriangle } from "lucide-react"; // Импортируем иконку
// Новый, стилизованный компонент для отображения ошибки
function ErrorFallback({ error }: FallbackProps) {
const { t } = useTranslation();
return (
<div role="alert" style={{ padding: 16 }}>
<h4>Something went wrong:(</h4>
<div role="alert" className="m-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-destructive">
<div className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
<h3 className="font-semibold">{t("Something went wrong")}</h3>
</div>
<pre>{error.message}</pre>
<pre className="mt-2 whitespace-pre-wrap rounded-md bg-destructive/10 p-2 text-xs font-mono">
{error.message}
</pre>
<details title="Error Stack">
<summary>Error Stack</summary>
<pre>{error.stack}</pre>
<details className="mt-4">
<summary className="cursor-pointer text-xs font-medium hover:underline">
{t("Error Stack")}
</summary>
<pre className="mt-2 whitespace-pre-wrap rounded-md bg-muted p-2 text-xs font-mono text-muted-foreground">
{error.stack}
</pre>
</details>
</div>
);

View File

@@ -1,38 +1,30 @@
import React from "react";
import { Box, styled } from "@mui/material";
import { cn } from "@root/lib/utils"; // Импортируем утилиту для объединения классов
type Props = {
label: string;
fontSize?: string;
width?: string;
padding?: string;
children?: React.ReactNode;
className?: string; // Пропс для дополнительной стилизации
};
export const BaseFieldset: React.FC<Props> = (props: Props) => {
const Fieldset = styled(Box)<{ component?: string }>(() => ({
position: "relative",
border: "1px solid #bbb",
borderRadius: "5px",
width: props.width ?? "auto",
padding: props.padding ?? "15px",
}));
const Label = styled("legend")(({ theme }) => ({
position: "absolute",
top: "-10px",
left: props.padding ?? "15px",
backgroundColor: theme.palette.background.paper,
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))",
color: theme.palette.text.primary,
fontSize: props.fontSize ?? "1em",
}));
export const BaseFieldset: React.FC<Props> = (props) => {
const { label, children, className } = props;
return (
<Fieldset component="fieldset">
<Label>{props.label}</Label>
{props.children}
</Fieldset>
// 1. Используем тег fieldset для семантики. Он позиционирован как relative.
<fieldset
className={cn(
"relative rounded-md border border-border p-4", // Базовые стили
className // Дополнительные классы от пользователя
)}
>
{/* 2. Используем legend. Он абсолютно спозиционирован относительно fieldset. */}
<legend className="absolute -top-2.5 left-3 bg-background px-1 text-sm text-muted-foreground">
{label}
</legend>
{/* 3. Здесь будет содержимое филдсета */}
{children}
</fieldset>
);
};

View File

@@ -1,32 +1,29 @@
import React from "react";
import { Box, CircularProgress } from "@mui/material";
import { BaseLoading } from "./base-loading"; // 1. Импортируем наш собственный компонент загрузки
import { cn } from "@root/lib/utils";
export interface BaseLoadingOverlayProps {
isLoading: boolean;
className?: string;
}
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
isLoading,
className,
}) => {
if (!isLoading) return null;
return (
<Box
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.7)",
zIndex: 1000,
}}
// 2. Заменяем Box на div и переводим sx в классы Tailwind
<div
className={cn(
"absolute inset-0 z-50 flex items-center justify-center bg-background/70 backdrop-blur-sm",
className
)}
>
<CircularProgress />
</Box>
{/* 3. Используем наш BaseLoading и делаем его немного больше */}
<BaseLoading className="h-8 w-8 text-primary" />
</div>
);
};

View File

@@ -1,48 +1,14 @@
import { styled } from "@mui/material";
import { Loader2 } from "lucide-react"; // 1. Импортируем стандартную иконку загрузки
import { cn } from "@root/lib/utils"; // Утилита для объединения классов
const Loading = styled("div")`
position: relative;
display: flex;
height: 100%;
min-height: 18px;
box-sizing: border-box;
align-items: center;
interface Props {
className?: string;
}
& > div {
box-sizing: border-box;
width: 6px;
height: 6px;
margin: 2px;
border-radius: 100%;
animation: loading 0.7s -0.15s infinite linear;
}
& > div:nth-child(2n-1) {
animation-delay: -0.5s;
}
@keyframes loading {
50% {
opacity: 0.2;
transform: scale(0.75);
}
100% {
opacity: 1;
transform: scale(1);
}
}
`;
const LoadingItem = styled("div")(({ theme }) => ({
background: theme.palette.text.secondary,
}));
export const BaseLoading = () => {
export const BaseLoading: React.FC<Props> = ({ className }) => {
return (
<Loading>
<LoadingItem />
<LoadingItem />
<LoadingItem />
</Loading>
// 2. Используем иконку с анимацией вращения от Tailwind
// Мы можем легко менять ее размер и цвет через className
<Loader2 className={cn("h-5 w-5 animate-spin", className)} />
);
};

View File

@@ -1,50 +1,40 @@
import React, { ReactNode } from "react";
import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary";
import { useTheme } from "@mui/material/styles";
import { cn } from "@root/lib/utils";
interface Props {
title?: React.ReactNode; // the page title
header?: React.ReactNode; // something behind title
contentStyle?: React.CSSProperties;
children?: ReactNode;
full?: boolean;
title?: ReactNode; // Заголовок страницы
header?: ReactNode; // Элементы в правой части шапки (кнопки и т.д.)
children?: ReactNode; // Основное содержимое страницы
className?: string; // Дополнительные классы для основной области контента
}
export const BasePage: React.FC<Props> = (props) => {
const { title, header, contentStyle, full, children } = props;
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const { title, header, children, className } = props;
return (
<BaseErrorBoundary>
<div className="base-page">
<header data-tauri-drag-region="true" style={{ userSelect: "none" }}>
<Typography
sx={{ fontSize: "20px", fontWeight: "700 " }}
data-tauri-drag-region="true"
>
{title}
</Typography>
{/* 1. Корневой контейнер: flex-колонка на всю высоту */}
<div className="h-full flex flex-col bg-background text-foreground">
{header}
{/* 2. Шапка: не растягивается, имеет фиксированную высоту и нижнюю границу */}
<header
data-tauri-drag-region="true"
className="flex-shrink-0 flex items-center justify-between h-16 px-4 border-b border-border"
>
<h2 className="text-xl font-bold" data-tauri-drag-region="true">
{title}
</h2>
<div data-tauri-drag-region="true">
{header}
</div>
</header>
<div
className={full ? "base-container no-padding" : "base-container"}
style={{ backgroundColor: isDark ? "#1e1f27" : "#ffffff" }}
>
<section
style={{
backgroundColor: isDark ? "#1e1f27" : "var(--background-color)",
}}
>
<div className="base-content" style={contentStyle}>
{children}
</div>
</section>
</div>
{/* 3. Основная область: занимает все оставшееся место и прокручивается */}
<main className={cn("flex-1 overflow-y-auto min-h-0", className)}>
{children}
</main>
</div>
</BaseErrorBoundary>
);

View File

@@ -1,11 +1,12 @@
import { Box, SvgIcon, TextField, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { ChangeEvent, useEffect, useRef, useState, useMemo } from "react";
import { ChangeEvent, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
import { cn } from "@root/lib/utils";
// Новые импорты
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { CaseSensitive, WholeWord, Regex } from "lucide-react"; // Иконки из lucide-react
export type SearchState = {
text: string;
@@ -16,150 +17,97 @@ export type SearchState = {
type SearchProps = {
placeholder?: string;
matchCase?: boolean;
matchWholeWord?: boolean;
useRegularExpression?: boolean;
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
};
const StyledTextField = styled(TextField)(({ theme }) => ({
"& .MuiInputBase-root": {
background: theme.palette.mode === "light" ? "#fff" : undefined,
paddingRight: "4px",
},
"& .MuiInputBase-root svg[aria-label='active'] path": {
fill: theme.palette.primary.light,
},
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
fill: "#A7A7A7",
},
}));
export const BaseSearchBox = (props: SearchProps) => {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
const [matchWholeWord, setMatchWholeWord] = useState(
props.matchWholeWord ?? false,
);
const [useRegularExpression, setUseRegularExpression] = useState(
props.useRegularExpression ?? false,
);
const [text, setText] = useState("");
const [matchCase, setMatchCase] = useState(false);
const [matchWholeWord, setMatchWholeWord] = useState(false);
const [useRegularExpression, setUseRegularExpression] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const iconStyle = {
style: {
height: "24px",
width: "24px",
cursor: "pointer",
} as React.CSSProperties,
inheritViewBox: true,
};
const createMatcher = useMemo(() => {
return (searchText: string) => {
try {
setErrorMessage(""); // Сбрасываем ошибку при новой попытке
return (content: string) => {
if (!searchText) return true;
let item = !matchCase ? content.toLowerCase() : content;
let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
const flags = matchCase ? "" : "i";
if (useRegularExpression) {
return new RegExp(searchItem).test(item);
return new RegExp(searchText, flags).test(content);
}
let pattern = searchText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // Экранируем спецсимволы
if (matchWholeWord) {
return new RegExp(`\\b${searchItem}\\b`).test(item);
pattern = `\\b${pattern}\\b`;
}
return item.includes(searchItem);
return new RegExp(pattern, flags).test(content);
};
} catch (err) {
setErrorMessage(`${err}`);
return () => true;
} catch (err: any) {
setErrorMessage(err.message);
return () => true; // Возвращаем "безопасный" матчер в случае ошибки
}
};
}, [matchCase, matchWholeWord, useRegularExpression]);
useEffect(() => {
if (!inputRef.current) return;
const value = inputRef.current.value;
setErrorMessage("");
props.onSearch(createMatcher(value), {
text: value,
matchCase,
matchWholeWord,
useRegularExpression,
});
}, [matchCase, matchWholeWord, useRegularExpression, createMatcher]);
props.onSearch(createMatcher(text), { text, matchCase, matchWholeWord, useRegularExpression });
}, [matchCase, matchWholeWord, useRegularExpression, createMatcher]); // Убрали text из зависимостей
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target?.value ?? "";
setErrorMessage("");
props.onSearch(createMatcher(value), {
text: value,
matchCase,
matchWholeWord,
useRegularExpression,
});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setText(value);
props.onSearch(createMatcher(value), { text: value, matchCase, matchWholeWord, useRegularExpression });
};
const getToggleVariant = (isActive: boolean) => (isActive ? "secondary" : "ghost");
return (
<Tooltip title={errorMessage} placement="bottom-start">
<StyledTextField
autoComplete="new-password"
inputRef={inputRef}
hiddenLabel
fullWidth
size="small"
variant="outlined"
spellCheck="false"
placeholder={props.placeholder ?? t("Filter conditions")}
sx={{ input: { py: 0.65, px: 1.25 } }}
onChange={onChange}
slotProps={{
input: {
sx: { pr: 1 },
endAdornment: (
<Box display="flex">
<Tooltip title={t("Match Case")}>
<div>
<SvgIcon
component={matchCaseIcon}
{...iconStyle}
aria-label={matchCase ? "active" : "inactive"}
onClick={() => setMatchCase(!matchCase)}
/>
</div>
</Tooltip>
<Tooltip title={t("Match Whole Word")}>
<div>
<SvgIcon
component={matchWholeWordIcon}
{...iconStyle}
aria-label={matchWholeWord ? "active" : "inactive"}
onClick={() => setMatchWholeWord(!matchWholeWord)}
/>
</div>
</Tooltip>
<Tooltip title={t("Use Regular Expression")}>
<div>
<SvgIcon
component={useRegularExpressionIcon}
aria-label={useRegularExpression ? "active" : "inactive"}
{...iconStyle}
onClick={() =>
setUseRegularExpression(!useRegularExpression)
}
/>{" "}
</div>
</Tooltip>
</Box>
),
},
}}
/>
</Tooltip>
<div className="w-full">
<div className="relative">
{/* Добавляем правый отступ, чтобы текст не заезжал под иконки */}
<Input
placeholder={props.placeholder ?? t("Filter conditions")}
value={text}
onChange={handleChange}
className="pr-28" // pr-[112px]
/>
{/* Контейнер для иконок, абсолютно спозиционированный справа */}
<div className="absolute inset-y-0 right-0 flex items-center pr-2">
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={getToggleVariant(matchCase)} size="icon" className="h-7 w-7" onClick={() => setMatchCase(!matchCase)}>
<CaseSensitive className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Match Case")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={getToggleVariant(matchWholeWord)} size="icon" className="h-7 w-7" onClick={() => setMatchWholeWord(!matchWholeWord)}>
<WholeWord className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Match Whole Word")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant={getToggleVariant(useRegularExpression)} size="icon" className="h-7 w-7" onClick={() => setUseRegularExpression(!useRegularExpression)}>
<Regex className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Use Regular Expression")}</p></TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
{/* Отображение ошибки под полем ввода */}
{errorMessage && <p className="mt-1 text-xs text-destructive">{errorMessage}</p>}
</div>
);
};

View File

@@ -1,19 +1,37 @@
import { Select, SelectProps, styled } from "@mui/material";
import * as React from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@root/lib/utils";
// Определяем новые пропсы для нашего компонента
export interface BaseStyledSelectProps {
children: React.ReactNode; // Сюда будут передаваться <SelectItem>
value?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
className?: string; // для дополнительной стилизации
}
export const BaseStyledSelect: React.FC<BaseStyledSelectProps> = (props) => {
const { value, onValueChange, placeholder, children, className } = props;
export const BaseStyledSelect = styled((props: SelectProps<string>) => {
return (
<Select
size="small"
autoComplete="new-password"
sx={{
width: 120,
height: 33.375,
mr: 1,
'[role="button"]': { py: 0.65 },
}}
{...props}
/>
// Используем композицию компонентов Select из shadcn/ui
<Select value={value} onValueChange={onValueChange}>
<SelectTrigger
className={cn(
"h-9 w-[180px]", // Задаем стандартные размеры, как у других селектов
className
)}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>{children}</SelectContent>
</Select>
);
})(({ theme }) => ({
background: theme.palette.mode === "light" ? "#fff" : undefined,
}));
};

View File

@@ -1,24 +1,32 @@
import { TextField, type TextFieldProps, styled } from "@mui/material";
import * as React from "react"; // 1. Убедимся, что React импортирован
import { useTranslation } from "react-i18next";
import { cn } from "@root/lib/utils";
import { Input } from "@/components/ui/input"; // 2. Убираем импорт несуществующего типа InputProps
export const BaseStyledTextField = styled((props: TextFieldProps) => {
// 3. Определяем наши пропсы, расширяя стандартный тип для input-элементов из React
export interface BaseStyledTextFieldProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
export const BaseStyledTextField = React.forwardRef<
HTMLInputElement,
BaseStyledTextFieldProps // Используем наш правильный тип
>((props, ref) => {
const { t } = useTranslation();
const { className, ...restProps } = props;
return (
<TextField
autoComplete="new-password"
hiddenLabel
fullWidth
size="small"
variant="outlined"
<Input
ref={ref}
className={cn(
"h-9", // Задаем стандартную компактную высоту
className
)}
placeholder={props.placeholder ?? t("Filter conditions")}
autoComplete="off"
spellCheck="false"
placeholder={t("Filter conditions")}
sx={{ input: { py: 0.65, px: 1.25 } }}
{...props}
{...restProps}
/>
);
})(({ theme }) => ({
"& .MuiInputBase-root": {
background: theme.palette.mode === "light" ? "#fff" : undefined,
},
}));
});
BaseStyledTextField.displayName = "BaseStyledTextField";

View File

@@ -1,58 +1,23 @@
import { styled } from "@mui/material/styles";
import { default as MuiSwitch, SwitchProps } from "@mui/material/Switch";
import * as React from "react";
import { Switch as ShadcnSwitch } from "@/components/ui/switch";
import { cn } from "@root/lib/utils";
export const Switch = styled((props: SwitchProps) => (
<MuiSwitch
focusVisibleClassName=".Mui-focusVisible"
disableRipple
{...props}
/>
))(({ theme }) => ({
width: 42,
height: 26,
padding: 0,
marginRight: 1,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: 2,
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(16px)",
color: "#fff",
"& + .MuiSwitch-track": {
backgroundColor: theme.palette.primary.main,
opacity: 1,
border: 0,
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: 0.5,
},
},
"&.Mui-focusVisible .MuiSwitch-thumb": {
color: "#33cf4d",
border: "6px solid #fff",
},
"&.Mui-disabled .MuiSwitch-thumb": {
color:
theme.palette.mode === "light"
? theme.palette.grey[100]
: theme.palette.grey[600],
},
"&.Mui-disabled + .MuiSwitch-track": {
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
},
},
"& .MuiSwitch-thumb": {
boxSizing: "border-box",
width: 22,
height: 22,
},
"& .MuiSwitch-track": {
borderRadius: 26 / 2,
backgroundColor: theme.palette.mode === "light" ? "#BBBBBB" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
}),
},
}));
// Тип пропсов остается без изменений
export type SwitchProps = React.ComponentPropsWithoutRef<typeof ShadcnSwitch>;
const Switch = React.forwardRef<
HTMLButtonElement,
SwitchProps
>(({ className, ...props }, ref) => {
return (
<ShadcnSwitch
className={cn(className)}
ref={ref}
{...props}
/>
);
});
Switch.displayName = "Switch";
export { Switch };

View File

@@ -1,24 +1,52 @@
import * as React from "react";
import { cn } from "@root/lib/utils";
// 1. Убираем импорт несуществующего типа ButtonProps
import { Button } from "@/components/ui/button";
import {
Tooltip,
IconButton,
IconButtonProps,
SvgIconProps,
} from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Info } from "lucide-react";
interface Props extends IconButtonProps {
title?: string;
icon?: React.ElementType<SvgIconProps>;
// 2. Определяем наши пропсы, расширяя стандартный тип для кнопок из React
export interface TooltipIconProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
tooltip: React.ReactNode;
icon?: React.ReactNode;
}
export const TooltipIcon: React.FC<Props> = (props: Props) => {
const { title = "", icon: Icon = InfoRounded, ...restProps } = props;
export const TooltipIcon = React.forwardRef<
HTMLButtonElement,
TooltipIconProps
>(({ tooltip, icon, className, ...props }, ref) => {
const displayIcon = icon || <Info className="h-4 w-4" />;
return (
<Tooltip title={title} placement="top">
<IconButton color="inherit" size="small" {...restProps}>
<Icon fontSize="inherit" style={{ cursor: "pointer", opacity: 0.75 }} />
</IconButton>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
ref={ref}
variant="ghost"
size="icon"
className={cn("h-7 w-7 text-muted-foreground", className)}
{...props}
>
{displayIcon}
<span className="sr-only">
{typeof tooltip === "string" ? tooltip : "Icon button"}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{typeof tooltip === "string" ? <p>{tooltip}</p> : tooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
});
TooltipIcon.displayName = "TooltipIcon";