重写所有页面和组件:用纯 Tailwind CSS 替换 MUI 组件和图标
This commit is contained in:
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import ToolCard from '@/pages/Dashboard/ToolCard';
|
import ToolCard from '@/pages/Dashboard/ToolCard';
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import { Clock } from 'lucide-react';
|
||||||
|
|
||||||
describe('ToolCard 组件', () => {
|
describe('ToolCard 组件', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -16,7 +16,7 @@ describe('ToolCard 组件', () => {
|
|||||||
title="测试工具"
|
title="测试工具"
|
||||||
description="这是一个测试工具"
|
description="这是一个测试工具"
|
||||||
colorKey="primary"
|
colorKey="primary"
|
||||||
icon={AccessTimeIcon}
|
icon={Clock}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -26,16 +26,14 @@ describe('ToolCard 组件', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('无描述时仅渲染标题', () => {
|
it('无描述时仅渲染标题', () => {
|
||||||
render(
|
render(<ToolCard title="仅标题" colorKey="primary" icon={Clock} onClick={() => {}} />);
|
||||||
<ToolCard title="仅标题" colorKey="primary" icon={AccessTimeIcon} onClick={() => {}} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText('仅标题')).toBeInTheDocument();
|
expect(screen.getByText('仅标题')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应渲染图标', () => {
|
it('应渲染图标', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ToolCard title="带图标" colorKey="primary" icon={AccessTimeIcon} onClick={() => {}} />,
|
<ToolCard title="带图标" colorKey="primary" icon={Clock} onClick={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
const svgElement = container.querySelector('svg');
|
const svgElement = container.querySelector('svg');
|
||||||
@@ -47,7 +45,7 @@ describe('ToolCard 组件', () => {
|
|||||||
<ToolCard
|
<ToolCard
|
||||||
title="带快照"
|
title="带快照"
|
||||||
colorKey="primary"
|
colorKey="primary"
|
||||||
icon={AccessTimeIcon}
|
icon={Clock}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
snapshot={<div data-testid="snapshot">快照内容</div>}
|
snapshot={<div data-testid="snapshot">快照内容</div>}
|
||||||
/>,
|
/>,
|
||||||
@@ -58,16 +56,14 @@ describe('ToolCard 组件', () => {
|
|||||||
|
|
||||||
it('未提供快照时不渲染快照区域', () => {
|
it('未提供快照时不渲染快照区域', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ToolCard title="无快照" colorKey="primary" icon={AccessTimeIcon} onClick={() => {}} />,
|
<ToolCard title="无快照" colorKey="primary" icon={Clock} onClick={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(container.querySelector('[data-testid="snapshot"]')).not.toBeInTheDocument();
|
expect(container.querySelector('[data-testid="snapshot"]')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应使用 CardActionArea 渲染,支持键盘聚焦', () => {
|
it('应使用 CardActionArea 渲染,支持键盘聚焦', () => {
|
||||||
render(
|
render(<ToolCard title="可聚焦" colorKey="primary" icon={Clock} onClick={() => {}} />);
|
||||||
<ToolCard title="可聚焦" colorKey="primary" icon={AccessTimeIcon} onClick={() => {}} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /可聚焦/ });
|
const button = screen.getByRole('button', { name: /可聚焦/ });
|
||||||
expect(button).toBeInTheDocument();
|
expect(button).toBeInTheDocument();
|
||||||
@@ -78,9 +74,7 @@ describe('ToolCard 组件', () => {
|
|||||||
describe('交互测试', () => {
|
describe('交互测试', () => {
|
||||||
it('点击时应调用 onClick', () => {
|
it('点击时应调用 onClick', () => {
|
||||||
const handleClick = vi.fn();
|
const handleClick = vi.fn();
|
||||||
render(
|
render(<ToolCard title="可点击" colorKey="primary" icon={Clock} onClick={handleClick} />);
|
||||||
<ToolCard title="可点击" colorKey="primary" icon={AccessTimeIcon} onClick={handleClick} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /可点击/ });
|
const button = screen.getByRole('button', { name: /可点击/ });
|
||||||
fireEvent.click(button);
|
fireEvent.click(button);
|
||||||
@@ -90,14 +84,7 @@ describe('ToolCard 组件', () => {
|
|||||||
|
|
||||||
it('按 Enter 键时应调用 onClick', async () => {
|
it('按 Enter 键时应调用 onClick', async () => {
|
||||||
const handleClick = vi.fn();
|
const handleClick = vi.fn();
|
||||||
render(
|
render(<ToolCard title="键盘可触发" colorKey="primary" icon={Clock} onClick={handleClick} />);
|
||||||
<ToolCard
|
|
||||||
title="键盘可触发"
|
|
||||||
colorKey="primary"
|
|
||||||
icon={AccessTimeIcon}
|
|
||||||
onClick={handleClick}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: /键盘可触发/ });
|
const button = screen.getByRole('button', { name: /键盘可触发/ });
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -112,7 +99,7 @@ describe('ToolCard 组件', () => {
|
|||||||
describe('样式测试', () => {
|
describe('样式测试', () => {
|
||||||
it('应应用自定义颜色代码', () => {
|
it('应应用自定义颜色代码', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ToolCard title="自定义颜色" colorKey="warning" icon={AccessTimeIcon} onClick={() => {}} />,
|
<ToolCard title="自定义颜色" colorKey="warning" icon={Clock} onClick={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
const svgElement = container.querySelector('svg');
|
const svgElement = container.querySelector('svg');
|
||||||
|
|||||||
+21
-34
@@ -1,15 +1,17 @@
|
|||||||
import { type ComponentType, lazy } from 'react';
|
import { type ComponentType, lazy } from 'react';
|
||||||
import type { SvgIconProps } from '@mui/material/SvgIcon';
|
import type { LucideProps } from 'lucide-react';
|
||||||
import type { PageType } from '@/types/storage';
|
import type { PageType } from '@/types/storage';
|
||||||
import { Clock } from 'lucide-react';
|
import {
|
||||||
import StorageIcon from '@mui/icons-material/Storage';
|
Clock,
|
||||||
import QrCodeIcon from '@mui/icons-material/QrCode';
|
Database,
|
||||||
import DescriptionIcon from '@mui/icons-material/Description';
|
QrCode,
|
||||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
FileText,
|
||||||
import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
|
Key,
|
||||||
import TransformIcon from '@mui/icons-material/Transform';
|
GitCompareArrows,
|
||||||
import CodeIcon from '@mui/icons-material/Code';
|
ArrowLeftRight,
|
||||||
import ArticleIcon from '@mui/icons-material/Article';
|
Code,
|
||||||
|
File,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info';
|
export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info';
|
||||||
|
|
||||||
@@ -25,31 +27,16 @@ const Base64ConverterPage = lazy(() => import('@/pages/Base64Converter'));
|
|||||||
const MarkdownToHtmlPage = lazy(() => import('@/pages/MarkdownToHtml'));
|
const MarkdownToHtmlPage = lazy(() => import('@/pages/MarkdownToHtml'));
|
||||||
const HtmlToMarkdownPage = lazy(() => import('@/pages/HtmlToMarkdown'));
|
const HtmlToMarkdownPage = lazy(() => import('@/pages/HtmlToMarkdown'));
|
||||||
|
|
||||||
/**
|
|
||||||
* 功能配置接口
|
|
||||||
*
|
|
||||||
* 整合了路由信息和仪表盘卡片元数据,作为功能的单一事实来源
|
|
||||||
*/
|
|
||||||
export interface FeatureConfig {
|
export interface FeatureConfig {
|
||||||
/** 页面类型标识 */
|
|
||||||
key: PageType;
|
key: PageType;
|
||||||
/** 功能名称翻译键 */
|
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
/** 功能描述翻译键 */
|
|
||||||
descriptionKey: string;
|
descriptionKey: string;
|
||||||
/** 主题颜色键(用于仪表盘卡片,映射到 theme.palette[key].main) */
|
|
||||||
themeColorKey?: PaletteColorKey;
|
themeColorKey?: PaletteColorKey;
|
||||||
/** 图标组件引用(用于仪表盘卡片,按需实例化) */
|
icon?: ComponentType<LucideProps>;
|
||||||
icon?: ComponentType<SvgIconProps>;
|
|
||||||
/** 默认是否在仪表盘显示 */
|
|
||||||
defaultVisible: boolean;
|
defaultVisible: boolean;
|
||||||
/** 不同显示模式对应的组件 */
|
|
||||||
components: {
|
components: {
|
||||||
/** 弹窗模式组件 */
|
|
||||||
popup: ComponentType;
|
popup: ComponentType;
|
||||||
/** 侧边栏模式组件 */
|
|
||||||
sidepanel: ComponentType;
|
sidepanel: ComponentType;
|
||||||
/** 标签页模式组件 */
|
|
||||||
tab: ComponentType;
|
tab: ComponentType;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -84,7 +71,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:storageCleaner.title',
|
labelKey: 'features:storageCleaner.title',
|
||||||
descriptionKey: 'features:storageCleaner.description',
|
descriptionKey: 'features:storageCleaner.description',
|
||||||
themeColorKey: 'warning',
|
themeColorKey: 'warning',
|
||||||
icon: StorageIcon,
|
icon: Database,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: StorageCleanerPage,
|
popup: StorageCleanerPage,
|
||||||
@@ -97,7 +84,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:qrCode.title',
|
labelKey: 'features:qrCode.title',
|
||||||
descriptionKey: 'features:qrCode.description',
|
descriptionKey: 'features:qrCode.description',
|
||||||
themeColorKey: 'success',
|
themeColorKey: 'success',
|
||||||
icon: QrCodeIcon,
|
icon: QrCode,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: QrCodePage,
|
popup: QrCodePage,
|
||||||
@@ -110,7 +97,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:textStatistics.title',
|
labelKey: 'features:textStatistics.title',
|
||||||
descriptionKey: 'features:textStatistics.description',
|
descriptionKey: 'features:textStatistics.description',
|
||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: DescriptionIcon,
|
icon: FileText,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: TextStatisticsPage,
|
popup: TextStatisticsPage,
|
||||||
@@ -123,7 +110,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:jwt.title',
|
labelKey: 'features:jwt.title',
|
||||||
descriptionKey: 'features:jwt.description',
|
descriptionKey: 'features:jwt.description',
|
||||||
themeColorKey: 'info',
|
themeColorKey: 'info',
|
||||||
icon: VpnKeyIcon,
|
icon: Key,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: JwtPage,
|
popup: JwtPage,
|
||||||
@@ -136,7 +123,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:jsonDiff.title',
|
labelKey: 'features:jsonDiff.title',
|
||||||
descriptionKey: 'features:jsonDiff.description',
|
descriptionKey: 'features:jsonDiff.description',
|
||||||
themeColorKey: 'primary',
|
themeColorKey: 'primary',
|
||||||
icon: CompareArrowsIcon,
|
icon: GitCompareArrows,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: JsonToolsPage,
|
popup: JsonToolsPage,
|
||||||
@@ -149,7 +136,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:base64Converter.title',
|
labelKey: 'features:base64Converter.title',
|
||||||
descriptionKey: 'features:base64Converter.description',
|
descriptionKey: 'features:base64Converter.description',
|
||||||
themeColorKey: 'info',
|
themeColorKey: 'info',
|
||||||
icon: TransformIcon,
|
icon: ArrowLeftRight,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: Base64ConverterPage,
|
popup: Base64ConverterPage,
|
||||||
@@ -162,7 +149,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:markdownToHtml.title',
|
labelKey: 'features:markdownToHtml.title',
|
||||||
descriptionKey: 'features:markdownToHtml.description',
|
descriptionKey: 'features:markdownToHtml.description',
|
||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: CodeIcon,
|
icon: Code,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: MarkdownToHtmlPage,
|
popup: MarkdownToHtmlPage,
|
||||||
@@ -175,7 +162,7 @@ export const FEATURES: FeatureConfig[] = [
|
|||||||
labelKey: 'features:htmlToMarkdown.title',
|
labelKey: 'features:htmlToMarkdown.title',
|
||||||
descriptionKey: 'features:htmlToMarkdown.description',
|
descriptionKey: 'features:htmlToMarkdown.description',
|
||||||
themeColorKey: 'secondary',
|
themeColorKey: 'secondary',
|
||||||
icon: ArticleIcon,
|
icon: File,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
components: {
|
components: {
|
||||||
popup: HtmlToMarkdownPage,
|
popup: HtmlToMarkdownPage,
|
||||||
|
|||||||
+95
-206
@@ -1,20 +1,5 @@
|
|||||||
import { SyntheticEvent, useEffect, useMemo, useState } from 'react';
|
import { SyntheticEvent, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import { Settings, RefreshCw, GripVertical } from 'lucide-react';
|
||||||
alpha,
|
|
||||||
Box,
|
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
Paper,
|
|
||||||
Stack,
|
|
||||||
Switch,
|
|
||||||
Tab,
|
|
||||||
Tabs,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
||||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@@ -43,12 +28,19 @@ import {
|
|||||||
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
import GlobalSnackbar, { useSnackbarState } from '@/components/GlobalSnackbar';
|
||||||
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
import PageErrorBoundary from '@/components/PageErrorBoundary';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import { useTheme, type PaletteColor, type Theme } from '@mui/material/styles';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { PaletteColorKey } from '@/config/features';
|
import type { PaletteColorKey } from '@/config/features';
|
||||||
|
|
||||||
const getPaletteColor = (theme: Theme, key: PaletteColorKey): PaletteColor =>
|
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
||||||
(theme.palette as unknown as Record<string, PaletteColor>)[key];
|
primary: '#1976d2',
|
||||||
|
success: '#2e7d32',
|
||||||
|
warning: '#e65100',
|
||||||
|
error: '#c62828',
|
||||||
|
secondary: '#9c27b0',
|
||||||
|
info: '#0288d1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColorCode = (key: PaletteColorKey): string => PALETTE_COLORS[key];
|
||||||
|
|
||||||
const isValidPage = (page: unknown): page is PageType => {
|
const isValidPage = (page: unknown): page is PageType => {
|
||||||
return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page);
|
return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page);
|
||||||
@@ -75,7 +67,6 @@ function SortableFeatureRow({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
onToggle,
|
onToggle,
|
||||||
}: SortableFeatureRowProps) {
|
}: SortableFeatureRowProps) {
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation(['features']);
|
const { t } = useTranslation(['features']);
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||||
id: pageKey,
|
id: pageKey,
|
||||||
@@ -85,131 +76,85 @@ function SortableFeatureRow({
|
|||||||
if (!feature) return null;
|
if (!feature) return null;
|
||||||
|
|
||||||
const colorKey = feature.themeColorKey ?? 'primary';
|
const colorKey = feature.themeColorKey ?? 'primary';
|
||||||
const colorCode = getPaletteColor(theme, colorKey).main;
|
const colorCode = getColorCode(colorKey);
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
transition,
|
transition,
|
||||||
zIndex: isDragging ? 1 : 'auto',
|
zIndex: isDragging ? 1 : 'auto',
|
||||||
position: 'relative' as const,
|
position: 'relative' as const,
|
||||||
|
backgroundColor: isDragging ? `${colorCode}0a` : 'transparent',
|
||||||
|
boxShadow: isDragging ? '0 8px 20px rgba(0,0,0,0.08)' : 'none',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
sx={{
|
className={`flex items-center justify-between p-4 sm:p-5 transition-all duration-200 hover:bg-gray-50 ${
|
||||||
display: 'flex',
|
isLast ? '' : 'border-b border-gray-200'
|
||||||
alignItems: 'center',
|
}`}
|
||||||
justifyContent: 'space-between',
|
|
||||||
p: { xs: 2, sm: 2.5 },
|
|
||||||
borderBottom: isLast ? 'none' : '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
bgcolor: isDragging ? alpha(colorCode, 0.04) : 'transparent',
|
|
||||||
boxShadow: isDragging ? `0 8px 20px ${alpha('#000', 0.08)}` : 'none',
|
|
||||||
transition: 'background-color 0.2s, box-shadow 0.2s',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: alpha(colorCode, 0.02),
|
|
||||||
},
|
|
||||||
'&:hover .drag-handle': {
|
|
||||||
color: 'text.secondary',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Stack
|
<div className="flex items-center gap-3 sm:gap-4 flex-1 min-w-0">
|
||||||
direction="row"
|
{/* 拖拽手柄 */}
|
||||||
spacing={{ xs: 1.5, sm: 2 }}
|
<div
|
||||||
alignItems="center"
|
className="drag-handle text-gray-300 cursor-grab touch-none transition-colors duration-200 hover:text-gray-500 active:cursor-grabbing"
|
||||||
sx={{ flex: 1, minWidth: 0 }}
|
|
||||||
>
|
|
||||||
{/* 拖拽手柄 - 整行可拖,手柄是视觉暗示 */}
|
|
||||||
<Box
|
|
||||||
className="drag-handle"
|
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
sx={{
|
|
||||||
color: alpha(theme.palette.text.primary, 0.2),
|
|
||||||
display: 'flex',
|
|
||||||
cursor: 'grab',
|
|
||||||
touchAction: 'none',
|
|
||||||
transition: 'color 0.2s',
|
|
||||||
'&:active': { cursor: 'grabbing' },
|
|
||||||
'&:hover': { color: 'text.secondary' },
|
|
||||||
}}
|
|
||||||
aria-label={`拖拽以调整 ${t(feature.labelKey)} 的位置`}
|
aria-label={`拖拽以调整 ${t(feature.labelKey)} 的位置`}
|
||||||
>
|
>
|
||||||
<DragIndicatorIcon fontSize="small" />
|
<GripVertical size={16} />
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{/* 功能图标 */}
|
{/* 功能图标 */}
|
||||||
<Box
|
<div
|
||||||
sx={{
|
className="flex items-center justify-center w-9 h-9 rounded-xl flex-shrink-0"
|
||||||
width: 36,
|
style={{
|
||||||
height: 36,
|
backgroundColor: `${colorCode}1a`,
|
||||||
borderRadius: 2.5,
|
|
||||||
bgcolor: alpha(colorCode, 0.1),
|
|
||||||
color: colorCode,
|
color: colorCode,
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{feature.icon && <feature.icon sx={{ fontSize: 20 }} />}
|
{feature.icon && <feature.icon size={20} />}
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{/* 文本信息 */}
|
{/* 文本信息 */}
|
||||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
<div className="min-w-0 flex-1">
|
||||||
<Typography
|
<div className="font-bold text-[0.95rem] leading-tight text-gray-900">
|
||||||
variant="subtitle2"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 700,
|
|
||||||
color: 'text.primary',
|
|
||||||
fontSize: '0.95rem',
|
|
||||||
lineHeight: 1.3,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t(feature.labelKey)}
|
{t(feature.labelKey)}
|
||||||
</Typography>
|
</div>
|
||||||
{feature.descriptionKey && (
|
{feature.descriptionKey && (
|
||||||
<Tooltip title={t(feature.descriptionKey)} placement="top-start" enterDelay={400}>
|
<div
|
||||||
<Typography
|
className="text-xs text-gray-500 font-medium mt-0.5 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
variant="caption"
|
title={t(feature.descriptionKey)}
|
||||||
color="text.secondary"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
mt: 0.25,
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
WebkitLineClamp: 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{t(feature.descriptionKey)}
|
{t(feature.descriptionKey)}
|
||||||
</Typography>
|
</div>
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
<Switch
|
{/* 开关 */}
|
||||||
color="primary"
|
<button
|
||||||
checked={isChecked}
|
type="button"
|
||||||
onChange={() => onToggle(pageKey)}
|
role="switch"
|
||||||
|
aria-checked={isChecked}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
onClick={() => onToggle(pageKey)}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
|
||||||
|
isChecked ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
} ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform duration-200 ${
|
||||||
|
isChecked ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Options 设置页面主组件
|
|
||||||
* 支持对不同窗口入口的功能显示和排序进行独立配置
|
|
||||||
*/
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const theme = useTheme();
|
|
||||||
const { t } = useTranslation(['features', 'common']);
|
const { t } = useTranslation(['features', 'common']);
|
||||||
|
|
||||||
const initialWindowType = useMemo(() => {
|
const initialWindowType = useMemo(() => {
|
||||||
@@ -360,120 +305,64 @@ export default function App() {
|
|||||||
|
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}
|
<div className="w-6 h-6 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
|
||||||
>
|
</div>
|
||||||
<CircularProgress size={24} />
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<div className="app min-h-screen bg-gray-50 flex flex-col">
|
||||||
className="app"
|
|
||||||
sx={{
|
|
||||||
minHeight: '100vh',
|
|
||||||
bgcolor: 'background.default',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PageErrorBoundary>
|
<PageErrorBoundary>
|
||||||
{/* 顶部标题与导航栏 */}
|
{/* 顶部标题与导航栏 */}
|
||||||
<Box
|
<div className="w-full bg-white border-b border-gray-200 pt-8 sm:pt-12 pb-0 px-4 sm:px-8">
|
||||||
sx={{
|
<div className="max-w-3xl mx-auto">
|
||||||
width: '100%',
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
pt: { xs: 3, sm: 5 },
|
|
||||||
pb: 0,
|
|
||||||
px: { xs: 2, sm: 4 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
icon={<SettingsIcon />}
|
icon={<Settings size={20} />}
|
||||||
iconColor={theme.palette.primary.main}
|
iconColor="#1976d2"
|
||||||
title="应用设置"
|
title="应用设置"
|
||||||
subtitle="针对不同窗口类型独立配置 Dashboard 中显示的功能及其排序"
|
subtitle="针对不同窗口类型独立配置 Dashboard 中显示的功能及其排序"
|
||||||
sx={{ marginBottom: '1rem' }}
|
sx={{ marginBottom: '1rem' }}
|
||||||
/>
|
/>
|
||||||
{/* Tab 与恢复按钮同行 */}
|
{/* Tab 与恢复按钮同行 */}
|
||||||
<Stack
|
<div className="flex items-end justify-between border-b-0">
|
||||||
direction="row"
|
<div className="flex-1 flex">
|
||||||
alignItems="flex-end"
|
{(['popup', 'sidepanel', 'tab'] as WindowType[]).map((type) => (
|
||||||
justifyContent="space-between"
|
<button
|
||||||
sx={{ borderBottom: 'none' }}
|
key={type}
|
||||||
|
onClick={(e) => handleWindowTypeChange(e, type)}
|
||||||
|
className={`px-4 sm:px-6 py-2 text-[0.9rem] font-bold transition-colors duration-200 ${
|
||||||
|
windowType === type
|
||||||
|
? 'text-blue-600 border-b-2 border-blue-600'
|
||||||
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Tabs
|
{type === 'popup' ? 'Popup 窗口' : type === 'sidepanel' ? '侧边栏' : '标签页'}
|
||||||
value={windowType}
|
</button>
|
||||||
onChange={handleWindowTypeChange}
|
))}
|
||||||
indicatorColor="primary"
|
</div>
|
||||||
textColor="primary"
|
<button
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 'auto',
|
|
||||||
'& .MuiTab-root': {
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
textTransform: 'none',
|
|
||||||
minWidth: { xs: 100, sm: 140 },
|
|
||||||
letterSpacing: '0.3px',
|
|
||||||
},
|
|
||||||
'& .MuiTabs-indicator': {
|
|
||||||
height: 3,
|
|
||||||
borderRadius: '3px 3px 0 0',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tab value="popup" label="Popup 窗口" />
|
|
||||||
<Tab value="sidepanel" label="侧边栏" />
|
|
||||||
<Tab value="tab" label="标签页" />
|
|
||||||
</Tabs>
|
|
||||||
<Tooltip title="恢复当前模式默认" placement="top">
|
|
||||||
<IconButton
|
|
||||||
onClick={handleRestoreDefaults}
|
onClick={handleRestoreDefaults}
|
||||||
size="small"
|
className="mb-1 ml-2 p-1.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors duration-200"
|
||||||
sx={{
|
title="恢复当前模式默认"
|
||||||
mb: 1,
|
|
||||||
ml: 1,
|
|
||||||
color: 'text.secondary',
|
|
||||||
'&:hover': {
|
|
||||||
color: 'primary.main',
|
|
||||||
bgcolor: alpha(theme.palette.primary.main, 0.08),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
aria-label="恢复当前模式默认"
|
|
||||||
>
|
>
|
||||||
<RefreshIcon fontSize="small" />
|
<RefreshCw size={16} />
|
||||||
</IconButton>
|
</button>
|
||||||
</Tooltip>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 主内容区域 */}
|
{/* 主内容区域 */}
|
||||||
<Box sx={{ flex: 1, p: { xs: 2, sm: 4 } }}>
|
<div className="flex-1 p-4 sm:p-8">
|
||||||
<Box sx={{ maxWidth: 800, mx: 'auto' }}>
|
<div className="max-w-3xl mx-auto">
|
||||||
<Paper
|
<div className="rounded-2xl border border-gray-200 overflow-hidden bg-white shadow-sm">
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
borderRadius: 4,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
overflow: 'hidden',
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
boxShadow: '0 4px 24px rgba(0,0,0,0.03)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext items={pageOrder} strategy={verticalListSortingStrategy}>
|
<SortableContext items={pageOrder} strategy={verticalListSortingStrategy}>
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
<div className="flex flex-col">
|
||||||
{pageOrder.map((key, index, array) => {
|
{pageOrder.map((key, index, array) => {
|
||||||
const isChecked = visiblePages.includes(key);
|
const isChecked = visiblePages.includes(key);
|
||||||
const isDisabled = isChecked && visiblePages.length === 1;
|
const isDisabled = isChecked && visiblePages.length === 1;
|
||||||
@@ -488,15 +377,15 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</Paper>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
</PageErrorBoundary>
|
</PageErrorBoundary>
|
||||||
|
|
||||||
<GlobalSnackbar {...snackbarProps} />
|
<GlobalSnackbar {...snackbarProps} />
|
||||||
</Box>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+522
-48
@@ -15,8 +15,9 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.8",
|
"@mui/icons-material": "^9.0.1",
|
||||||
"@mui/material": "^7.3.8",
|
"@mui/material": "^9.0.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@vitest/coverage-v8": "^4.1.7",
|
"@vitest/coverage-v8": "^4.1.7",
|
||||||
@@ -1762,9 +1763,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "7.3.11",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.1.tgz",
|
||||||
"integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==",
|
"integrity": "sha512-GzamIIhZ1bH77dq7eKaeyRgJdkypsxin4jBFq2EMs4lBWRR0LFO1CSVMsoebn/VvjcNrnrOrjy48MkrkQUK2iw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1772,12 +1773,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/icons-material": {
|
"node_modules/@mui/icons-material": {
|
||||||
"version": "7.3.11",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.0.1.tgz",
|
||||||
"integrity": "sha512-+hz5ilwHZ3djd5es3sCErLioqe/NhZcYTsV/TNXZAMdJdb23F4xzJjqnnZdnurc3S1+ietcssRNqieOhPQLZ7Q==",
|
"integrity": "sha512-5PRpQjVLTNLyV/2J9J53Yz4R0tVbodG0BQDN2zQI1QBG1OPYM25ar+4N20eyFOfJT6zKglLzsnU70+zdVLaTkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6"
|
"@babel/runtime": "^7.29.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -1787,7 +1788,7 @@
|
|||||||
"url": "https://opencollective.com/mui-org"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mui/material": "^7.3.11",
|
"@mui/material": "^9.0.1",
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
@@ -1798,22 +1799,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/material": {
|
||||||
"version": "7.3.11",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-9.0.1.tgz",
|
||||||
"integrity": "sha512-yq8bPc3LxOwKRWpcjRgDkYFmpM6aKlARfESTmOQcvLYFeJwtHte2tw6hJDrb8sk8wcvpDprHEHVaoUU0MslIkw==",
|
"integrity": "sha512-voyCpeUxcSWLN7KPZuq0pGCIt726T9K6kiVM3XUcywZDAlZSarLHaUxJVQpospbjjOzN53hwyjo8s6KoWl6utw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@mui/core-downloads-tracker": "^7.3.11",
|
"@mui/core-downloads-tracker": "^9.0.1",
|
||||||
"@mui/system": "^7.3.11",
|
"@mui/system": "^9.0.1",
|
||||||
"@mui/types": "^7.4.12",
|
"@mui/types": "^9.0.0",
|
||||||
"@mui/utils": "^7.3.11",
|
"@mui/utils": "^9.0.1",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"csstype": "^3.2.3",
|
"csstype": "^3.2.3",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-is": "^19.2.3",
|
"react-is": "^19.2.4",
|
||||||
"react-transition-group": "^4.4.5"
|
"react-transition-group": "^4.4.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1826,7 +1827,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.5.0",
|
"@emotion/react": "^11.5.0",
|
||||||
"@emotion/styled": "^11.3.0",
|
"@emotion/styled": "^11.3.0",
|
||||||
"@mui/material-pigment-css": "^7.3.11",
|
"@mui/material-pigment-css": "^9.0.1",
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -1847,13 +1848,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/private-theming": {
|
"node_modules/@mui/private-theming": {
|
||||||
"version": "7.3.11",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.0.1.tgz",
|
||||||
"integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==",
|
"integrity": "sha512-pSIGq4Yw749KHEwlkYZWVERgHgwJELP6ODtBNUfV8V4oIb5H+h7IQDFXuk/b2oQccODK1enJAtiEzlgLZmq+8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@mui/utils": "^7.3.11",
|
"@mui/utils": "^9.0.1",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1874,12 +1875,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/styled-engine": {
|
"node_modules/@mui/styled-engine": {
|
||||||
"version": "7.3.10",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.0.0.tgz",
|
||||||
"integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==",
|
"integrity": "sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@emotion/cache": "^11.14.0",
|
"@emotion/cache": "^11.14.0",
|
||||||
"@emotion/serialize": "^1.3.3",
|
"@emotion/serialize": "^1.3.3",
|
||||||
"@emotion/sheet": "^1.4.0",
|
"@emotion/sheet": "^1.4.0",
|
||||||
@@ -1908,16 +1909,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "7.3.11",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/system/-/system-9.0.1.tgz",
|
||||||
"integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==",
|
"integrity": "sha512-WvlioaLxk6ewUIOfh0StxUvOPDS1mCfzaulcudsL1brZNXuh0N9FMk7RpH7ImJKjEz412SEy/V/yvqmtxbqxCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@mui/private-theming": "^7.3.11",
|
"@mui/private-theming": "^9.0.1",
|
||||||
"@mui/styled-engine": "^7.3.10",
|
"@mui/styled-engine": "^9.0.0",
|
||||||
"@mui/types": "^7.4.12",
|
"@mui/types": "^9.0.0",
|
||||||
"@mui/utils": "^7.3.11",
|
"@mui/utils": "^9.0.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"csstype": "^3.2.3",
|
"csstype": "^3.2.3",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
@@ -1948,12 +1949,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/types": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.4.12",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/types/-/types-9.0.0.tgz",
|
||||||
"integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==",
|
"integrity": "sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6"
|
"@babel/runtime": "^7.29.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -1965,17 +1966,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/utils": {
|
"node_modules/@mui/utils": {
|
||||||
"version": "7.3.11",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.0.1.tgz",
|
||||||
"integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==",
|
"integrity": "sha512-f3UO3jNN1pYg5zxqXC81Bvv8hx5ACcYc0387382ZI7M5ono1heIwHYLrKsz85myguWdeVKPRZGmDdynWUBjK2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.6",
|
"@babel/runtime": "^7.29.2",
|
||||||
"@mui/types": "^7.4.12",
|
"@mui/types": "^9.0.0",
|
||||||
"@types/prop-types": "^15.7.15",
|
"@types/prop-types": "^15.7.15",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-is": "^19.2.3"
|
"react-is": "^19.2.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -2114,6 +2115,12 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -2129,6 +2136,249 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||||
@@ -2147,6 +2397,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
|
||||||
@@ -2635,7 +2970,7 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -3333,6 +3668,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
@@ -4750,6 +5097,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-node-es": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -6061,6 +6414,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-nonce": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-port-please": {
|
"node_modules/get-port-please": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
|
||||||
@@ -9600,6 +9962,75 @@
|
|||||||
"integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==",
|
"integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
|
"react-style-singleton": "^2.2.3",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.3",
|
||||||
|
"use-sidecar": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
@@ -11414,6 +11845,49 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
"node_modules/use-sync-external-store": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
|||||||
+3
-2
@@ -26,8 +26,9 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.8",
|
"@mui/icons-material": "^9.0.1",
|
||||||
"@mui/material": "^7.3.8",
|
"@mui/material": "^9.0.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@vitest/coverage-v8": "^4.1.7",
|
"@vitest/coverage-v8": "^4.1.7",
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
/**
|
|
||||||
* ToolCard 组件 - 工具卡片
|
|
||||||
*
|
|
||||||
* 用于在仪表盘中展示各个工具功能的卡片组件,支持图标、标题、描述、
|
|
||||||
* 快照内容展示,具备悬停动画效果。
|
|
||||||
*/
|
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { SvgIconProps } from '@mui/material/SvgIcon';
|
import type { LucideProps } from 'lucide-react';
|
||||||
import type { PaletteColorKey } from '@/config/features';
|
import type { PaletteColorKey } from '@/config/features';
|
||||||
|
|
||||||
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
const PALETTE_COLORS: Record<PaletteColorKey, string> = {
|
||||||
@@ -23,7 +17,7 @@ interface ToolCardProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
snapshot?: React.ReactNode;
|
snapshot?: React.ReactNode;
|
||||||
colorKey: PaletteColorKey;
|
colorKey: PaletteColorKey;
|
||||||
icon: ComponentType<SvgIconProps>;
|
icon: ComponentType<LucideProps>;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +62,7 @@ export default function ToolCard({
|
|||||||
color: colorCode,
|
color: colorCode,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconComponent sx={{ fontSize: 20 }} />
|
<IconComponent size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-bold text-sm leading-tight text-gray-900 flex items-center gap-1">
|
<span className="font-bold text-sm leading-tight text-gray-900 flex items-center gap-1">
|
||||||
|
|||||||
+50
-174
@@ -1,18 +1,5 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import {
|
import { Code, Download, Trash2 } from 'lucide-react';
|
||||||
Alert,
|
|
||||||
alpha,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
Paper,
|
|
||||||
} from '@mui/material';
|
|
||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
|
||||||
import CodeIcon from '@mui/icons-material/Code';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
@@ -57,18 +44,12 @@ export default function HtmlToMarkdownPage() {
|
|||||||
const showOutput = previewMode !== 'markdown';
|
const showOutput = previewMode !== 'markdown';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{ py: 3 }}>
|
<div className="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
<PageHeader title={t('pageTitle')} subtitle={t('pageSubtitle')} icon={<CodeIcon />} />
|
<PageHeader title={t('pageTitle')} subtitle={t('pageSubtitle')} icon={<Code size={20} />} />
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<div className="flex flex-col gap-4">
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<Stack
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
flexWrap="wrap"
|
|
||||||
gap={1.5}
|
|
||||||
>
|
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={previewMode}
|
value={previewMode}
|
||||||
options={[
|
options={[
|
||||||
@@ -80,193 +61,88 @@ export default function HtmlToMarkdownPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
<div className="flex gap-2">
|
||||||
<Button
|
<button
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
startIcon={<DownloadIcon />}
|
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={!result.markdown}
|
disabled={!result.markdown}
|
||||||
sx={{ borderRadius: 2 }}
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
|
<Download size={14} />
|
||||||
{t('download')}
|
{t('download')}
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
startIcon={<DeleteOutlineIcon />}
|
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
sx={{ borderRadius: 2 }}
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
{t('clear')}
|
{t('clear')}
|
||||||
</Button>
|
</button>
|
||||||
</Stack>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* 错误提示 */}
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ borderRadius: 2 }}>
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<Box
|
<div
|
||||||
sx={{
|
className={`grid gap-4 min-h-[500px] ${
|
||||||
display: 'grid',
|
showInput && showOutput ? 'md:grid-cols-2' : 'grid-cols-1'
|
||||||
gridTemplateColumns: {
|
}`}
|
||||||
xs: '1fr',
|
|
||||||
md: showInput && showOutput ? '1fr 1fr' : '1fr',
|
|
||||||
},
|
|
||||||
gap: 2,
|
|
||||||
minHeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* HTML 输入区 */}
|
{/* HTML 输入区 */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<Paper
|
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col">
|
||||||
elevation={0}
|
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200">
|
||||||
sx={{
|
<span className="text-xs font-bold text-gray-500">{t('inputLabel')}</span>
|
||||||
border: '1px solid',
|
<span className="text-xs text-gray-400">
|
||||||
borderColor: 'divider',
|
|
||||||
borderRadius: 3,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.04),
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
|
||||||
{t('inputLabel')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
|
|
||||||
{t('charCount', { count: html.length })}
|
{t('charCount', { count: html.length })}
|
||||||
</Typography>
|
</span>
|
||||||
</Box>
|
</div>
|
||||||
<TextField
|
<textarea
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
value={html}
|
value={html}
|
||||||
onChange={(e) => setHtml(e.target.value)}
|
onChange={(e) => setHtml(e.target.value)}
|
||||||
placeholder={t('inputPlaceholder')}
|
placeholder={t('inputPlaceholder')}
|
||||||
sx={{
|
className="flex-1 min-h-[400px] p-4 font-mono text-[0.85rem] leading-relaxed resize-none focus:outline-none"
|
||||||
flex: 1,
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
borderRadius: 0,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
'& fieldset': { border: 'none' },
|
|
||||||
},
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
py: 2,
|
|
||||||
px: 2,
|
|
||||||
minHeight: 400,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Markdown 输出区 */}
|
{/* Markdown 输出区 */}
|
||||||
{showOutput && (
|
{showOutput && (
|
||||||
<Paper
|
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col">
|
||||||
elevation={0}
|
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200">
|
||||||
sx={{
|
<span className="text-xs font-bold text-gray-500">
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
borderRadius: 3,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.04),
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
|
||||||
{(previewMode as string) === 'markdown'
|
{(previewMode as string) === 'markdown'
|
||||||
? t('markdownOutputLabel')
|
? t('markdownOutputLabel')
|
||||||
: t('previewLabel')}
|
: t('previewLabel')}
|
||||||
</Typography>
|
</span>
|
||||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
<div className="flex items-center gap-1">
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
|
<span className="text-xs text-gray-400">
|
||||||
{t('charCount', { count: result.markdownLength })}
|
{t('charCount', { count: result.markdownLength })}
|
||||||
</Typography>
|
</span>
|
||||||
<CopyButton text={result.markdown} size="small" />
|
<CopyButton text={result.markdown} size="small" />
|
||||||
</Stack>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{(previewMode as string) === 'markdown' ? (
|
{(previewMode as string) === 'markdown' ? (
|
||||||
<TextField
|
<textarea
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
value={result.markdown}
|
value={result.markdown}
|
||||||
slotProps={{ input: { readOnly: true } }}
|
readOnly
|
||||||
sx={{
|
className="flex-1 min-h-[400px] p-4 font-mono text-[0.85rem] leading-relaxed resize-none focus:outline-none bg-gray-50"
|
||||||
flex: 1,
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
borderRadius: 0,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
'& fieldset': { border: 'none' },
|
|
||||||
},
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
py: 2,
|
|
||||||
px: 2,
|
|
||||||
minHeight: 400,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<div className="flex-1 p-4 min-h-[400px] overflow-auto bg-white font-mono text-[0.85rem] leading-relaxed whitespace-pre-wrap break-words">
|
||||||
sx={{
|
{result.markdown || <span className="text-gray-400">{t('emptyHint')}</span>}
|
||||||
flex: 1,
|
</div>
|
||||||
p: 2,
|
|
||||||
minHeight: 400,
|
|
||||||
overflow: 'auto',
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{result.markdown || (
|
|
||||||
<Typography variant="body2" color="text.disabled">
|
|
||||||
{t('emptyHint')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-179
@@ -1,19 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import { Code, Trash2, Printer, Download } from 'lucide-react';
|
||||||
Alert,
|
|
||||||
alpha,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
Paper,
|
|
||||||
} from '@mui/material';
|
|
||||||
import CodeIcon from '@mui/icons-material/Code';
|
|
||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
|
||||||
import PrintIcon from '@mui/icons-material/Print';
|
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
|
||||||
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
import { useLazyTranslation } from '@/utils/useLazyTranslation';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import CopyButton from '@/components/CopyButton';
|
import CopyButton from '@/components/CopyButton';
|
||||||
@@ -121,7 +107,6 @@ export default function MarkdownToHtmlPage() {
|
|||||||
const result = useMemo(() => markdownToHtml(markdown), [markdown]);
|
const result = useMemo(() => markdownToHtml(markdown), [markdown]);
|
||||||
const error = result.hasError ? (result.error ?? null) : null;
|
const error = result.hasError ? (result.error ?? null) : null;
|
||||||
|
|
||||||
// 更新 iframe 预览内容
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const iframe = iframeRef.current;
|
const iframe = iframeRef.current;
|
||||||
if (!iframe || !iframe.contentDocument) return;
|
if (!iframe || !iframe.contentDocument) return;
|
||||||
@@ -163,18 +148,12 @@ export default function MarkdownToHtmlPage() {
|
|||||||
const showPreview = previewMode !== 'html';
|
const showPreview = previewMode !== 'html';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{ py: 3 }}>
|
<div className="container mx-auto px-4 py-6 max-w-7xl">
|
||||||
<PageHeader title={t('pageTitle')} subtitle={t('pageSubtitle')} icon={<CodeIcon />} />
|
<PageHeader title={t('pageTitle')} subtitle={t('pageSubtitle')} icon={<Code size={20} />} />
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<div className="flex flex-col gap-4">
|
||||||
{/* 工具栏 */}
|
{/* 工具栏 */}
|
||||||
<Stack
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
alignItems="center"
|
|
||||||
flexWrap="wrap"
|
|
||||||
gap={1.5}
|
|
||||||
>
|
|
||||||
<SwitchButtonGroup
|
<SwitchButtonGroup
|
||||||
value={previewMode}
|
value={previewMode}
|
||||||
options={[
|
options={[
|
||||||
@@ -186,200 +165,96 @@ export default function MarkdownToHtmlPage() {
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
<div className="flex gap-2">
|
||||||
<Button
|
<button
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
startIcon={<DeleteOutlineIcon />}
|
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
sx={{ borderRadius: 2 }}
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
{t('clear')}
|
{t('clear')}
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
startIcon={<PrintIcon />}
|
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
sx={{ borderRadius: 2 }}
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
<Printer size={14} />
|
||||||
{t('print')}
|
{t('print')}
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
startIcon={<DownloadIcon />}
|
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
sx={{ borderRadius: 2 }}
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
|
<Download size={14} />
|
||||||
{t('download')}
|
{t('download')}
|
||||||
</Button>
|
</button>
|
||||||
</Stack>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
|
|
||||||
{/* 错误提示 */}
|
{/* 错误提示 */}
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ borderRadius: 2 }}>
|
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 主内容区 */}
|
{/* 主内容区 */}
|
||||||
<Box
|
<div
|
||||||
sx={{
|
className={`grid gap-4 min-h-[500px] ${
|
||||||
display: 'grid',
|
showInput && showPreview ? 'md:grid-cols-2' : 'grid-cols-1'
|
||||||
gridTemplateColumns: {
|
}`}
|
||||||
xs: '1fr',
|
|
||||||
md: showInput && showPreview ? '1fr 1fr' : '1fr',
|
|
||||||
},
|
|
||||||
gap: 2,
|
|
||||||
minHeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Markdown 输入区 */}
|
{/* Markdown 输入区 */}
|
||||||
{showInput && (
|
{showInput && (
|
||||||
<Paper
|
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col">
|
||||||
elevation={0}
|
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200">
|
||||||
sx={{
|
<span className="text-xs font-bold text-gray-500">{t('inputLabel')}</span>
|
||||||
border: '1px solid',
|
<span className="text-xs text-gray-400">
|
||||||
borderColor: 'divider',
|
|
||||||
borderRadius: 3,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.04),
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
|
||||||
{t('inputLabel')}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
|
|
||||||
{t('charCount', { count: markdown.length })}
|
{t('charCount', { count: markdown.length })}
|
||||||
</Typography>
|
</span>
|
||||||
</Box>
|
</div>
|
||||||
<TextField
|
<textarea
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
value={markdown}
|
value={markdown}
|
||||||
onChange={(e) => setMarkdown(e.target.value)}
|
onChange={(e) => setMarkdown(e.target.value)}
|
||||||
placeholder={t('inputPlaceholder')}
|
placeholder={t('inputPlaceholder')}
|
||||||
sx={{
|
className="flex-1 min-h-[400px] p-4 font-mono text-[0.85rem] leading-relaxed resize-none focus:outline-none"
|
||||||
flex: 1,
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
borderRadius: 0,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
lineHeight: 1.6,
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
'& fieldset': { border: 'none' },
|
|
||||||
},
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
py: 2,
|
|
||||||
px: 2,
|
|
||||||
minHeight: 400,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 预览/输出区 */}
|
{/* 预览/输出区 */}
|
||||||
{showPreview && (
|
{showPreview && (
|
||||||
<Paper
|
<div className="border border-gray-200 rounded-xl overflow-hidden flex flex-col">
|
||||||
elevation={0}
|
<div className="flex items-center justify-between px-4 py-2 bg-blue-50/50 border-b border-gray-200">
|
||||||
sx={{
|
<span className="text-xs font-bold text-gray-500">
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
borderRadius: 3,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
px: 2,
|
|
||||||
py: 1,
|
|
||||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.04),
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary' }}>
|
|
||||||
{(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')}
|
{(previewMode as string) === 'html' ? t('htmlOutputLabel') : t('previewLabel')}
|
||||||
</Typography>
|
</span>
|
||||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
<div className="flex items-center gap-1">
|
||||||
<Typography variant="caption" sx={{ color: 'text.disabled' }}>
|
<span className="text-xs text-gray-400">
|
||||||
{t('charCount', { count: result.htmlLength })}
|
{t('charCount', { count: result.htmlLength })}
|
||||||
</Typography>
|
</span>
|
||||||
<CopyButton text={result.html} size="small" />
|
<CopyButton text={result.html} size="small" />
|
||||||
</Stack>
|
</div>
|
||||||
</Box>
|
</div>
|
||||||
|
|
||||||
{(previewMode as string) === 'html' ? (
|
{(previewMode as string) === 'html' ? (
|
||||||
<TextField
|
<textarea
|
||||||
multiline
|
|
||||||
fullWidth
|
|
||||||
value={result.html}
|
value={result.html}
|
||||||
InputProps={{ readOnly: true }}
|
readOnly
|
||||||
sx={{
|
className="flex-1 min-h-[400px] p-4 font-mono text-[0.8rem] leading-relaxed resize-none focus:outline-none bg-gray-50"
|
||||||
flex: 1,
|
|
||||||
'& .MuiOutlinedInput-root': {
|
|
||||||
borderRadius: 0,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
lineHeight: 1.5,
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
'& fieldset': { border: 'none' },
|
|
||||||
},
|
|
||||||
'& .MuiInputBase-input': {
|
|
||||||
py: 2,
|
|
||||||
px: 2,
|
|
||||||
minHeight: 400,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<div className="flex-1 p-4 min-h-[400px] overflow-auto bg-white">
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
p: 2,
|
|
||||||
minHeight: 400,
|
|
||||||
overflow: 'auto',
|
|
||||||
bgcolor: 'background.paper',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
title="markdown-preview"
|
title="markdown-preview"
|
||||||
style={{
|
className="w-full h-full min-h-[380px] border-none bg-transparent"
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
minHeight: 380,
|
|
||||||
border: 'none',
|
|
||||||
background: 'transparent',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</div>
|
||||||
</Stack>
|
</div>
|
||||||
</Container>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user