diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 735cc3c..e6cc6fb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,10 @@ "mcp__Claude_in_Chrome__tabs_context_mcp", "Bash(git init *)", "Bash(git add *)", - "Bash(git commit -m ' *)" + "Bash(git commit -m ' *)", + "Bash(git remote *)", + "Bash(git push *)", + "Bash(git commit -m 'docs: 重写 README 为中文,添加英文版跳转链接 *)" ] } } diff --git a/src/components/admin/MarkdownToolbar.jsx b/src/components/admin/MarkdownToolbar.jsx index 64ebac2..876e02d 100644 --- a/src/components/admin/MarkdownToolbar.jsx +++ b/src/components/admin/MarkdownToolbar.jsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react'; +import { useState, useRef, useCallback } from 'react'; import styles from './MarkdownToolbar.module.css'; const TOOLS = [ @@ -9,13 +9,32 @@ const TOOLS = [ { label: '', title: '代码', wrap: (s) => `${s}` }, ]; +const CLOSE_DURATION = 160; + export default function MarkdownToolbar({ textareaRef }) { const [imgPopover, setImgPopover] = useState(false); + const [popoverClosing, setPopoverClosing] = useState(false); const [imgUrl, setImgUrl] = useState(''); const [imgAlt, setImgAlt] = useState(''); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); + const closePopover = useCallback(() => { + setPopoverClosing(true); + setTimeout(() => { + setImgPopover(false); + setPopoverClosing(false); + }, CLOSE_DURATION); + }, []); + + const togglePopover = useCallback(() => { + if (imgPopover) { + closePopover(); + } else { + setImgPopover(true); + } + }, [imgPopover, closePopover]); + const insertAt = (html) => { const ta = textareaRef.current; if (!ta) return; @@ -52,7 +71,7 @@ export default function MarkdownToolbar({ textareaRef }) { insertAt(`${alt}`); setImgUrl(''); setImgAlt(''); - setImgPopover(false); + closePopover(); }; const handleFileChange = (e) => { @@ -69,7 +88,7 @@ export default function MarkdownToolbar({ textareaRef }) { const alt = file.name.replace(/\.[^.]+$/, '') || '图片'; insertAt(`${alt}`); setUploading(false); - setImgPopover(false); + closePopover(); e.target.value = ''; }; reader.readAsDataURL(file); @@ -94,16 +113,16 @@ export default function MarkdownToolbar({ textareaRef }) { {imgPopover && ( -
+

插入图片

@@ -162,7 +181,7 @@ export default function MarkdownToolbar({ textareaRef }) { diff --git a/src/components/admin/MarkdownToolbar.module.css b/src/components/admin/MarkdownToolbar.module.css index 25bbfcd..1037ea1 100644 --- a/src/components/admin/MarkdownToolbar.module.css +++ b/src/components/admin/MarkdownToolbar.module.css @@ -32,6 +32,12 @@ border-color: var(--color-muted); } +.toolActive { + background: var(--color-surface-2); + color: var(--color-ink); + border-color: var(--color-muted); +} + .separator { width: 1px; height: 20px; @@ -51,7 +57,17 @@ display: flex; flex-direction: column; gap: var(--space-4); - animation: slideDown 200ms cubic-bezier(0.34, 1.56, 0.64, 1); + animation: slideDown 200ms cubic-bezier(0.34, 1.56, 0.64, 1) both; + transform-origin: top left; +} + +.popoverOut { + animation: popoverCollapse 160ms cubic-bezier(0.4, 0, 1, 1) both; +} + +@keyframes popoverCollapse { + from { opacity: 1; transform: translateY(0) scale(1); } + to { opacity: 0; transform: translateY(-8px) scale(0.96); } } .popoverTitle { diff --git a/src/components/layout/AppShell.jsx b/src/components/layout/AppShell.jsx index 10a1671..0f67a3f 100644 --- a/src/components/layout/AppShell.jsx +++ b/src/components/layout/AppShell.jsx @@ -3,26 +3,46 @@ import { useState, useEffect, useRef } from 'react'; import Header from './Header'; import Footer from './Footer'; import PetalCanvas from '../ui/PetalCanvas'; +import styles from './AppShell.module.css'; export default function AppShell() { const location = useLocation(); const prevPath = useRef(location.pathname); const [petalTrigger, setPetalTrigger] = useState(0); + // Track location key to re-trigger enter animation on every navigation + const [displayKey, setDisplayKey] = useState(location.key); + const [exiting, setExiting] = useState(false); + const exitTimer = useRef(null); + useEffect(() => { - if (prevPath.current !== location.pathname) { - prevPath.current = location.pathname; - setPetalTrigger(t => t + 1); + if (prevPath.current === location.pathname) return; + prevPath.current = location.pathname; + setPetalTrigger(t => t + 1); + + // Play exit on old content, then swap to new + setExiting(true); + clearTimeout(exitTimer.current); + exitTimer.current = setTimeout(() => { window.scrollTo({ top: 0, behavior: 'instant' }); - } - }, [location.pathname]); + setDisplayKey(location.key); + setExiting(false); + }, 160); + + return () => clearTimeout(exitTimer.current); + }, [location.pathname, location.key]); return ( <>
- +
+ +