New Interface (initial commit)
This commit is contained in:
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user