* feat: optimize popup standalone window layout and enhance storage cleaner synchronization

* docs: 更新README文档并删除过时文件

- 更新README文档,添加项目结构、功能特性和路由系统等详细信息
- 删除不再使用的文档文件,包括CLAUDE.md、GEMINI.md和多个设计规范文档
- 清理项目中的过时配置文件和计划文档

* feat: 添加 Vitest 测试框架和组件测试

- 添加 Vitest 配置 (vitest.config.ts, vitest.setup.ts)
- 创建组件测试: Button, ToolCard, GlobalSnackbar, TopBar, RouterContainer, StorageCleanerConfirm
- 创建工具测试: routes, storageCleaner
- 修复 background.ts 监听器参数问题
- 修复 options/App.tsx 硬编码默认值
- 更新 lint-staged.config.mjs (添加 .mjs 支持, 添加 --no-warn-ignored)
- 更新 tsconfig.json (添加测试类型支持, 移除测试文件排除)
- 更新 package.json (添加测试脚本和依赖)

* fix: 修复 StorageCleanerPage Chrome API 监听器内存泄漏

使用 useRef 模式存储 loadInfo 函数引用,避免依赖数组变化导致的监听器重复注册问题

* refactor(popup): 优化 OpenUrl 页面样式和导航逻辑

重构 OpenUrl 页面输入框样式,改进聚焦状态效果
移除 RouterProvider 依赖,直接通过存储设置侧边栏路由
在 OpenUrlViewer 页面添加加载状态指示器和错误处理
监听存储变化实现 URL 自动更新

* feat(ui): 优化存储清理页面UI和交互效果

重构存储清理页面组件,增强视觉层次和交互体验:
- 使用新的错误提示样式和布局
- 改进选项卡片样式,增加悬停动画和选中状态
- 调整整体间距和排版,提升视觉一致性
- 添加微交互效果如悬停缩放和阴影
- 优化颜色方案和过渡动画
- 统一组件尺寸和字体层级

* feat: 添加二维码工具页面,支持URL转二维码和二维码解析功能

* chore: update package-lock.json (npm audit fix)

* refactor(主题): 将页面样式抽离到统一配置文件

将各页面的颜色和样式配置抽离到config/pageTheme.ts中统一管理
优化测试用例中使用each替代forEach
更新路由测试以包含新的qrCode页面

* feat(二维码页面): 添加复制二维码功能并优化样式

添加复制二维码到剪贴板的功能,并调整按钮布局和样式。同时将 ContentCopyIcon 导入位置调整到其他图标导入之后,并修复缩进问题。在 tsconfig.json 中添加 vitest/globals 类型支持。

* feat(theme): 为所有页面添加统一的背景色和卡片背景色

为应用中的所有页面添加了统一的浅灰色背景(#f5f5f5)和白色卡片背景(#ffffff),以保持视觉一致性。修改了ToolCard组件以支持自定义卡片背景色,并更新了所有相关页面使用新的主题配置。

* feat: 添加复制按钮组件并优化现有复制功能

refactor(utils): 创建剪贴板工具函数
feat(components): 新增可复用的CopyButton组件
refactor(pages): 在QrCodePage和TimestampPage中使用CopyButton
style: 格式化代码并调整部分样式

* refactor(存储): 统一qrCode相关存储键名

将'qrCode/expanded'重命名为'qrCode/qrExpanded'以保持命名一致性

* feat: 添加二维码工具功能并更新项目配置

- 新增二维码工具页面及相关组件和工具函数
- 添加 MIT 许可证文件
- 更新 package.json 配置为公开项目
- 更新 README 文档说明新功能
This commit is contained in:
LingandRX
2026-04-19 15:31:14 +08:00
committed by GitHub
parent bbd0507bcd
commit be5e2f02ee
43 changed files with 4943 additions and 2483 deletions
+289 -230
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import dayjs from '@/utils/dayjs';
import {
TextField,
@@ -8,120 +8,115 @@ import {
Typography,
Box,
IconButton,
alpha,
Tooltip,
Theme,
Container,
Fade,
Divider,
alpha,
} from '@mui/material';
import GlobalSnackbar, { useSnackbar } from '@/components/GlobalSnackbar';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CheckIcon from '@mui/icons-material/Check';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import Button from '@/components/Button';
// ================= 常量配置 =================
const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss';
const ZONES = ['Asia/Shanghai', 'America/New_York', 'Europe/London'] as const;
type UnitType = 'ms' | 's';
type ZoneType = (typeof ZONES)[number];
const INPUT_STYLE = {
'& .MuiOutlinedInput-root': {
bgcolor: 'background.paper',
borderRadius: 3.5,
border: '1px solid',
borderColor: 'grey.100',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
'& fieldset': { border: 'none' },
'&:hover': { borderColor: 'grey.300', bgcolor: 'grey.50' },
'&.Mui-focused': {
bgcolor: '#fff',
borderColor: 'primary.main',
boxShadow: (theme: Theme) => `0 0 0 4px ${alpha(theme.palette.primary.main, 0.1)}`,
},
'&.Mui-error': {
borderColor: 'error.main',
boxShadow: (theme: Theme) => `0 0 0 4px ${alpha(theme.palette.error.main, 0.1)}`,
},
},
'& .MuiInputBase-input': {
py: 1.4,
px: 2,
fontSize: '0.9rem',
fontFamily: 'monospace',
fontWeight: 600
},
};
import CopyButton from '@/components/CopyButton';
import { DATE_FORMAT, ZONES, timestampPageStyles } from '@/config/pageTheme';
import type { UnitType, ZoneType } from '@/config/pageTheme';
// ================= 子组件:实时时钟 (优化交互) =================
interface LiveClockProps {
unit: UnitType;
onCopy: (val: string) => void;
onUseNow: (val: number) => void;
onUnitChange: (u: UnitType) => void;
showMessage?: (message: string, options?: { severity: 'success' | 'error' }) => void;
}
const LiveClock = React.memo(({
unit,
onCopy,
onUseNow,
onUnitChange
}: LiveClockProps) => {
const LiveClock = React.memo(({ unit, onUseNow, onUnitChange, showMessage }: LiveClockProps) => {
const [now, setNow] = useState(() => Date.now());
const onUseNowRef = useRef(onUseNow);
const showMessageRef = useRef(showMessage);
useEffect(() => {
onUseNowRef.current = onUseNow;
showMessageRef.current = showMessage;
}, [onUseNow, showMessage]);
useEffect(() => {
const t = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(t);
}, []);
const displayVal = useMemo(() =>
String(Math.floor(now / (unit === 'ms' ? 1 : 1000))),
[now, unit]);
const displayVal = useMemo(
() => String(Math.floor(now / (unit === 'ms' ? 1 : 1000))),
[now, unit],
);
const handleUseNow = useCallback(() => {
onUseNowRef.current(now);
}, [now]);
return (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
p: 1.8,
mb: 2.5,
bgcolor: alpha('#2196f3', 0.04),
borderRadius: 4,
border: '1px solid',
borderColor: alpha('#2196f3', 0.1)
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
p: 1.8,
mb: 2.5,
bgcolor: alpha('#2196f3', 0.04),
borderRadius: 4,
border: '1px solid',
borderColor: alpha('#2196f3', 0.1),
}}
>
<Stack spacing={0.5}>
<Typography variant="caption" sx={{ color: 'primary.main', fontWeight: 800, fontSize: '0.6rem', textTransform: 'uppercase', letterSpacing: 1 }}>
<Typography
variant="caption"
sx={{
color: 'primary.main',
fontWeight: 800,
fontSize: '0.6rem',
textTransform: 'uppercase',
letterSpacing: 1,
}}
>
</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: 'text.primary', fontFamily: 'monospace', fontSize: '1.2rem', letterSpacing: '-0.5px', lineHeight: 1.2 }}>
<Typography
variant="subtitle2"
sx={{
fontWeight: 800,
color: 'text.primary',
fontFamily: 'monospace',
fontSize: '1.2rem',
letterSpacing: '-0.5px',
lineHeight: 1.2,
}}
>
{displayVal}
</Typography>
</Stack>
<Stack direction="row" spacing={1} alignItems="center">
{/* 胶囊式单位切换器 */}
<Box sx={{
display: 'flex',
p: 0.4,
bgcolor: alpha('#2196f3', 0.08),
borderRadius: 2.5,
border: '1px solid',
borderColor: alpha('#2196f3', 0.1)
}}>
<Box
sx={{
display: 'flex',
p: 0.4,
bgcolor: alpha('#2196f3', 0.08),
borderRadius: 2.5,
border: '1px solid',
borderColor: alpha('#2196f3', 0.1),
}}
>
{(['ms', 's'] as const).map((u) => (
<Box
key={u}
onClick={() => onUnitChange(u)}
sx={{
px: 1.2,
py: 0.35,
borderRadius: 2,
sx={{
px: 1.2,
py: 0.35,
borderRadius: 2,
cursor: 'pointer',
fontSize: '0.65rem',
fontSize: '0.65rem',
fontWeight: 900,
transition: 'all 0.2s',
bgcolor: unit === u ? '#fff' : 'transparent',
@@ -133,26 +128,35 @@ const LiveClock = React.memo(({
</Box>
))}
</Box>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5, my: 1, borderColor: alpha('#2196f3', 0.1) }} />
<Divider
orientation="vertical"
flexItem
sx={{ mx: 0.5, my: 1, borderColor: alpha('#2196f3', 0.1) }}
/>
<Stack direction="row" spacing={0.5}>
<Tooltip title="填充到下方">
<IconButton
size="small"
onClick={() => onUseNow(now)}
sx={{ color: 'primary.main', bgcolor: '#fff', boxShadow: '0 2px 4px rgba(0,0,0,0.05)', '&:hover': { bgcolor: 'primary.main', color: '#fff' } }}
<IconButton
size="small"
onClick={handleUseNow}
sx={{
color: timestampPageStyles.primaryColor,
bgcolor: '#fff',
boxShadow: '0 2px 4px rgba(0,0,0,0.05)',
'&:hover': { bgcolor: timestampPageStyles.primaryColor, color: '#fff' },
}}
>
<AccessTimeIcon fontSize="small" />
</IconButton>
</Tooltip>
<IconButton
size="small"
onClick={() => onCopy(displayVal)}
sx={{ color: 'grey.400', '&:hover': { color: 'primary.main' } }}
>
<ContentCopyIcon fontSize="small" />
</IconButton>
<CopyButton
text={displayVal}
tooltip="复制时间戳"
size="small"
color={timestampPageStyles.primaryColor}
showMessage={showMessage}
/>
</Stack>
</Stack>
</Box>
@@ -167,28 +171,19 @@ interface ResultViewProps {
mode: 'ts2dt' | 'dt2ts';
unit: UnitType;
zone: string;
onCopy: (val: string) => void;
showMessage?: (message: string, options?: { severity: 'success' | 'error' }) => void;
}
const ResultView = React.memo(({
result,
mode,
unit,
zone,
onCopy
}: ResultViewProps) => {
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
onCopy(result);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}, [onCopy, result]);
const ResultView = React.memo(({ result, mode, unit, zone, showMessage }: ResultViewProps) => {
const extraInfo = useMemo(() => {
if (!result) return null;
const d = mode === 'ts2dt' ? dayjs(result, DATE_FORMAT).tz(zone) : (unit === 'ms' ? dayjs(Number(result)) : dayjs.unix(Number(result)));
const d =
mode === 'ts2dt'
? dayjs(result, DATE_FORMAT).tz(zone)
: unit === 'ms'
? dayjs(Number(result))
: dayjs.unix(Number(result));
return {
relative: d.fromNow(),
iso: d.toISOString(),
@@ -201,72 +196,95 @@ const ResultView = React.memo(({
return (
<Fade in={!!result}>
<Box sx={{ mt: 3, pt: 2.5, borderTop: '1px solid', borderColor: 'grey.50' }}>
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1.2, display: 'block', fontWeight: 800, fontSize: '0.7rem' }}>
<Typography
variant="caption"
sx={{
color: 'text.secondary',
mb: 1.2,
display: 'block',
fontWeight: 800,
fontSize: '0.7rem',
}}
>
</Typography>
<Box sx={{
bgcolor: alpha('#2196f3', 0.05),
p: 2,
borderRadius: 4,
position: 'relative',
mb: 2.5,
border: '1px solid',
borderColor: alpha('#2196f3', 0.1)
}}>
<Typography
variant="body1"
sx={{
fontFamily: 'monospace',
fontWeight: 700,
<Box
sx={{
bgcolor: alpha('#2196f3', 0.05),
p: 2,
borderRadius: 4,
position: 'relative',
mb: 2.5,
border: '1px solid',
borderColor: alpha('#2196f3', 0.1),
}}
>
<Typography
variant="body1"
sx={{
fontFamily: 'monospace',
fontWeight: 700,
color: 'primary.main',
wordBreak: 'break-all',
pr: 4,
fontSize: '1rem'
fontSize: '1rem',
}}
>
{result}
</Typography>
<IconButton
size="small"
onClick={handleCopy}
sx={{
position: 'absolute',
right: 8,
top: '50%',
<CopyButton
text={result}
tooltip="复制结果"
size="small"
color={timestampPageStyles.primaryColor}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
color: copied ? 'success.main' : 'primary.main',
bgcolor: '#fff',
boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
'&:hover': { bgcolor: copied ? 'success.main' : 'primary.main', color: '#fff' }
}}
>
{copied ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
</IconButton>
/>
</Box>
<Stack spacing={1.2}>
{[
{ label: '相对时间', value: extraInfo?.relative },
{ label: 'ISO 8601', value: extraInfo?.iso },
{ label: 'UTC 时间', value: extraInfo?.utc },
].map((item) => (
<Box key={item.label} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 1 }}>
<Typography variant="caption" sx={{ color: 'text.disabled', fontWeight: 700, fontSize: '0.65rem' }}>{item.label}</Typography>
<Typography
variant="caption"
onClick={() => { if (item.value) onCopy(item.value); }}
sx={{
fontFamily: 'monospace',
color: 'text.secondary',
fontWeight: 600,
fontSize: '0.65rem',
cursor: 'pointer',
'&:hover': { color: 'primary.main' }
}}
<Box
key={item.label}
sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 1 }}
>
<Typography
variant="caption"
sx={{ color: 'text.disabled', fontWeight: 700, fontSize: '0.65rem' }}
>
{item.value}
{item.label}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography
variant="caption"
sx={{
fontFamily: 'monospace',
color: 'text.secondary',
fontWeight: 600,
fontSize: '0.65rem',
}}
>
{item.value}
</Typography>
{item.value && (
<CopyButton
text={item.value}
tooltip="复制"
size="small"
color="primary"
showMessage={showMessage}
/>
)}
</Box>
</Box>
))}
</Stack>
@@ -288,30 +306,30 @@ export default function TimestampPage() {
const [error, setError] = useState('');
const { snackbarProps, showMessage } = useSnackbar({ autoHideDuration: 1500 });
const copy = useCallback(async (text: string) => {
try {
await navigator.clipboard.writeText(text);
showMessage('已复制', { severity: 'success' });
} catch {
showMessage('复制失败', { severity: 'error' });
}
}, [showMessage]);
const convert = useCallback(() => {
if (mode === 'ts2dt') {
const rawInput = tsInput.trim();
if (!rawInput) return;
const num = Number(rawInput);
if (isNaN(num)) { setError('无效数字'); return; }
if (isNaN(num)) {
setError('无效数字');
return;
}
const d = unit === 'ms' ? dayjs(num) : dayjs.unix(num);
if (!d.isValid()) { setError('无效时间戳'); return; }
if (!d.isValid()) {
setError('无效时间戳');
return;
}
setError('');
setResult(d.tz(zone).format(DATE_FORMAT));
} else {
const rawInput = dtInput.trim();
if (!rawInput) return;
const d = dayjs.tz(rawInput, DATE_FORMAT, zone);
if (!d.isValid()) { setError('格式错误'); return; }
if (!d.isValid()) {
setError('格式错误');
return;
}
setError('');
const ms = d.valueOf();
setResult(unit === 'ms' ? String(ms) : String(Math.floor(ms / 1000)));
@@ -323,24 +341,39 @@ export default function TimestampPage() {
return () => clearTimeout(timer);
}, [convert]);
const handleUseNow = useCallback((now: number) => {
if (mode === 'ts2dt') {
setTsInput(String(unit === 'ms' ? now : Math.floor(now / 1000)));
} else {
setDtInput(dayjs(now).tz(zone).format(DATE_FORMAT));
}
}, [mode, unit, zone]);
const handleUseNow = useCallback(
(now: number) => {
if (mode === 'ts2dt') {
setTsInput(String(unit === 'ms' ? now : Math.floor(now / 1000)));
} else {
setDtInput(dayjs(now).tz(zone).format(DATE_FORMAT));
}
},
[mode, unit, zone],
);
return (
<Box sx={{ pb: 3 }}>
<Box sx={{ bgcolor: '#f5f5f5', minHeight: '100%', pb: 3 }}>
<Container sx={{ py: 2 }}>
{/* Header with Icon */}
<Stack direction="row" spacing={1.5} alignItems="center" sx={{ mb: 2.5 }}>
<Box sx={{ p: 1, borderRadius: 2.5, bgcolor: alpha('#2196f3', 0.1), color: 'primary.main', display: 'flex' }}>
<Box
sx={{
p: 1,
borderRadius: 2.5,
bgcolor: alpha('#2196f3', 0.1),
color: 'primary.main',
display: 'flex',
}}
>
<AccessTimeIcon sx={{ fontSize: 20 }} />
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" fontWeight={900} sx={{ letterSpacing: '-0.5px', lineHeight: 1.2 }}>
<Typography
variant="subtitle1"
fontWeight={900}
sx={{ letterSpacing: '-0.5px', lineHeight: 1.2 }}
>
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
@@ -350,45 +383,59 @@ export default function TimestampPage() {
</Stack>
{/* Live Clock Card */}
<LiveClock unit={unit} onCopy={copy} onUseNow={handleUseNow} onUnitChange={setUnit} />
<LiveClock
unit={unit}
onUseNow={handleUseNow}
onUnitChange={setUnit}
showMessage={showMessage}
/>
{/* Mode Switcher */}
<Box sx={{
position: 'relative',
display: 'flex',
p: 0.6,
bgcolor: 'grey.100',
borderRadius: 4,
mb: 2.5,
border: '1px solid',
borderColor: 'grey.200'
}}>
<Box sx={{
position: 'absolute',
height: 'calc(100% - 10px)',
width: 'calc(50% - 5px)',
bgcolor: '#fff',
borderRadius: 3.5,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
transform: mode === 'ts2dt' ? 'translateX(0)' : 'translateX(100%)',
top: 5, left: 5,
}} />
<Box
sx={{
position: 'relative',
display: 'flex',
p: 0.6,
bgcolor: 'grey.100',
borderRadius: 4,
mb: 2.5,
border: '1px solid',
borderColor: 'grey.200',
}}
>
<Box
sx={{
position: 'absolute',
height: 'calc(100% - 10px)',
width: 'calc(50% - 5px)',
bgcolor: '#fff',
borderRadius: 3.5,
boxShadow: '0 4px 12px rgba(0,0,0,0.08)',
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
transform: mode === 'ts2dt' ? 'translateX(0)' : 'translateX(100%)',
top: 5,
left: 5,
}}
/>
{(['ts2dt', 'dt2ts'] as const).map((m) => (
<Box
key={m}
onClick={() => { setMode(m); setError(''); setResult(''); }}
sx={{
flex: 1,
py: 1,
textAlign: 'center',
position: 'relative',
zIndex: 1,
onClick={() => {
setMode(m);
setError('');
setResult('');
}}
sx={{
flex: 1,
py: 1,
textAlign: 'center',
position: 'relative',
zIndex: 1,
cursor: 'pointer',
fontWeight: 800,
fontSize: '0.75rem',
color: mode === m ? 'primary.main' : 'text.secondary',
transition: 'color 0.3s'
transition: 'color 0.3s',
}}
>
{m === 'ts2dt' ? '时间戳 → 日期' : '日期 → 时间戳'}
@@ -399,7 +446,7 @@ export default function TimestampPage() {
{/* Input Area */}
<Stack spacing={2} sx={{ mb: 3 }}>
<TextField
placeholder={mode === 'ts2dt' ? "输入时间戳..." : DATE_FORMAT}
placeholder={mode === 'ts2dt' ? '输入时间戳...' : DATE_FORMAT}
value={mode === 'ts2dt' ? tsInput : dtInput}
onChange={(e) => {
const val = e.target.value;
@@ -413,31 +460,33 @@ export default function TimestampPage() {
error={!!error}
helperText={error}
fullWidth
sx={INPUT_STYLE}
sx={timestampPageStyles.INPUT_STYLE}
/>
<Stack direction="row" spacing={1.5}>
{/* 优化后的单位选择按钮组 */}
<Box sx={{
flex: 1,
display: 'flex',
bgcolor: 'grey.50',
p: 0.5,
borderRadius: 3.5,
border: '1px solid',
borderColor: 'grey.100'
}}>
<Box
sx={{
flex: 1,
display: 'flex',
bgcolor: 'grey.50',
p: 0.5,
borderRadius: 3.5,
border: '1px solid',
borderColor: 'grey.100',
}}
>
{(['ms', 's'] as const).map((u) => (
<Box
key={u}
onClick={() => setUnit(u)}
sx={{
flex: 1,
py: 0.8,
textAlign: 'center',
borderRadius: 3,
sx={{
flex: 1,
py: 0.8,
textAlign: 'center',
borderRadius: 3,
cursor: 'pointer',
fontSize: '0.75rem',
fontSize: '0.75rem',
fontWeight: 800,
transition: 'all 0.2s',
bgcolor: unit === u ? '#fff' : 'transparent',
@@ -451,13 +500,20 @@ export default function TimestampPage() {
</Box>
<Select
fullWidth value={zone}
fullWidth
value={zone}
onChange={(e) => setZone(e.target.value as ZoneType)}
sx={{ ...INPUT_STYLE, flex: 1 }}
MenuProps={{ PaperProps: { sx: { borderRadius: 3, mt: 1, boxShadow: '0 12px 32px rgba(0,0,0,0.1)' } } }}
sx={{ ...timestampPageStyles.INPUT_STYLE, flex: 1 }}
MenuProps={{
PaperProps: {
sx: { borderRadius: 3, mt: 1, boxShadow: '0 12px 32px rgba(0,0,0,0.1)' },
},
}}
>
{ZONES.map((z) => (
<MenuItem key={z} value={z} sx={{ fontSize: '0.8rem', fontWeight: 600 }}>{z}</MenuItem>
<MenuItem key={z} value={z} sx={{ fontSize: '0.8rem', fontWeight: 600 }}>
{z}
</MenuItem>
))}
</Select>
</Stack>
@@ -465,8 +521,8 @@ export default function TimestampPage() {
{/* Main Action */}
<Button
fullWidth
variant="contained"
fullWidth
variant="contained"
onClick={convert}
sx={{
py: 1.4,
@@ -475,14 +531,17 @@ export default function TimestampPage() {
fontWeight: 800,
fontSize: '0.9rem',
boxShadow: 'none',
'&:hover': { bgcolor: 'primary.dark', boxShadow: `0 8px 24px ${alpha('#2196f3', 0.2)}` }
'&:hover': {
bgcolor: 'primary.dark',
boxShadow: `0 8px 24px ${alpha('#2196f3', 0.2)}`,
},
}}
>
</Button>
{/* Result View */}
<ResultView result={result} mode={mode} unit={unit} zone={zone} onCopy={copy} />
<ResultView result={result} mode={mode} unit={unit} zone={zone} showMessage={showMessage} />
</Container>
<GlobalSnackbar {...snackbarProps} />