fix(ux): address critic review on mobile landscape, dual hud fade and clipboard feedback
- Optimize short/landscape screen (< 520px height) viewports with compact padding, shrinkable containers and zero-overflow layout - Synchronize dual-HUD immersive fading for both Header and Controls with 3s stillness and global wake-up interactions - Enhance PasteModal with user-friendly error alerts and auto-focusing textarea on clipboard denial Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
44
src/App.tsx
44
src/App.tsx
@@ -215,7 +215,7 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse activity detection for HUD fading
|
||||
// Mouse/touch/key activity detection for HUD fading
|
||||
const resetInactivity = useCallback(() => {
|
||||
setIsUserActive(true);
|
||||
if (inactivityTimerRef.current) {
|
||||
@@ -224,7 +224,7 @@ export default function App() {
|
||||
if (isPlaying && settings.autoHideControls) {
|
||||
inactivityTimerRef.current = window.setTimeout(() => {
|
||||
setIsUserActive(false);
|
||||
}, 2200);
|
||||
}, 3000);
|
||||
}
|
||||
}, [isPlaying, settings.autoHideControls]);
|
||||
|
||||
@@ -235,11 +235,28 @@ export default function App() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const handleInteraction = () => {
|
||||
resetInactivity();
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleInteraction);
|
||||
window.addEventListener('keydown', handleInteraction);
|
||||
window.addEventListener('touchstart', handleInteraction);
|
||||
window.addEventListener('pointerdown', handleInteraction);
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setIsUserActive(false);
|
||||
}, 2200);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [isPlaying, settings.autoHideControls]);
|
||||
}, 3000);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
window.removeEventListener('mousemove', handleInteraction);
|
||||
window.removeEventListener('keydown', handleInteraction);
|
||||
window.removeEventListener('touchstart', handleInteraction);
|
||||
window.removeEventListener('pointerdown', handleInteraction);
|
||||
};
|
||||
}, [isPlaying, settings.autoHideControls, resetInactivity]);
|
||||
|
||||
// Keyboard shortcuts
|
||||
const anyModalOpen =
|
||||
@@ -278,6 +295,9 @@ export default function App() {
|
||||
|
||||
const themeClass = `theme-${settings.theme}`;
|
||||
const isHudDimmed = isPlaying && !isUserActive && settings.autoHideControls;
|
||||
const hudClasses = isHudDimmed
|
||||
? 'opacity-0 pointer-events-none transition-opacity duration-500'
|
||||
: 'opacity-100 pointer-events-auto transition-opacity duration-300';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -286,11 +306,7 @@ export default function App() {
|
||||
className={`relative w-full h-full flex flex-col justify-between overflow-hidden transition-colors duration-300 font-sans ${themeClass}`}
|
||||
>
|
||||
{/* Top Header HUD */}
|
||||
<div
|
||||
className={`w-full z-20 transition-opacity duration-500 ${
|
||||
isHudDimmed ? 'opacity-0 hover:opacity-100' : 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-full z-20 shrink-0 ${hudClasses}`}>
|
||||
<Header
|
||||
book={activeBook}
|
||||
currentChapter={currentChapter}
|
||||
@@ -307,7 +323,7 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
{/* Main RSVP Focus Arena */}
|
||||
<main className="flex-1 flex flex-col items-center justify-center px-4 z-10 w-full my-auto">
|
||||
<main className="flex-1 min-h-0 flex flex-col items-center justify-center px-2 sm:px-4 z-10 w-full overflow-hidden my-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center space-y-3 text-neutral-400">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-rose-500 border-t-transparent animate-spin" />
|
||||
@@ -330,11 +346,7 @@ export default function App() {
|
||||
</main>
|
||||
|
||||
{/* Bottom Controls HUD */}
|
||||
<div
|
||||
className={`w-full pb-6 z-20 transition-opacity duration-500 ${
|
||||
isHudDimmed ? 'opacity-10 hover:opacity-100' : 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-full pb-2 sm:pb-6 short:pb-1 z-20 shrink-0 ${hudClasses}`}>
|
||||
<Controls
|
||||
currentIndex={currentIndex}
|
||||
totalTokens={totalTokens}
|
||||
|
||||
@@ -59,11 +59,11 @@ export const Controls: React.FC<ControlsProps> = ({
|
||||
const remainingText = formatDuration(remainingSeconds);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-3xl mx-auto flex flex-col space-y-4 px-4 select-none">
|
||||
<div className="w-full max-w-3xl mx-auto flex flex-col space-y-2 sm:space-y-4 short:space-y-1 px-3 sm:px-4 select-none shrink-0">
|
||||
{/* Progress & Timeline Bar */}
|
||||
{settings.showProgressBar && (
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center text-xs font-mono text-neutral-400 dark:text-neutral-500">
|
||||
<div className="flex flex-col space-y-0.5 sm:space-y-1.5">
|
||||
<div className="flex justify-between items-center text-[10px] sm:text-xs font-mono text-neutral-400 dark:text-neutral-500">
|
||||
<span>
|
||||
{currentIndex + 1} / {totalTokens}{' '}
|
||||
<span className="opacity-75">({percent.toFixed(1)}%)</span>
|
||||
@@ -73,7 +73,7 @@ export const Controls: React.FC<ControlsProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-center group py-1 cursor-pointer">
|
||||
<div className="relative flex items-center group py-0.5 sm:py-1 cursor-pointer">
|
||||
{/* Chapter tick marks on the track */}
|
||||
{chapters.length > 1 &&
|
||||
chapters.map((chap, idx) => {
|
||||
@@ -95,19 +95,19 @@ export const Controls: React.FC<ControlsProps> = ({
|
||||
max={Math.max(0, totalTokens - 1)}
|
||||
value={currentIndex}
|
||||
onChange={(e) => onJumpTo(Number(e.target.value))}
|
||||
className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-rose-500 transition-all focus:outline-none"
|
||||
className="w-full h-1 sm:h-1.5 bg-neutral-200 dark:bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-rose-500 transition-all focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Playback Buttons */}
|
||||
<div className="flex items-center justify-between sm:justify-center sm:space-x-4">
|
||||
<div className="flex items-center justify-between sm:justify-center sm:space-x-4 short:space-x-2">
|
||||
{/* Restart */}
|
||||
<button
|
||||
onClick={onRestart}
|
||||
title="重新开始 [R]"
|
||||
className="p-2 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1.5 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -116,30 +116,30 @@ export const Controls: React.FC<ControlsProps> = ({
|
||||
<button
|
||||
onClick={() => onJumpBy(-10)}
|
||||
title="后退 10 词 [←]"
|
||||
className="p-2 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1.5 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronsLeft className="w-5 h-5" />
|
||||
<ChevronsLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</button>
|
||||
|
||||
{/* Previous Sentence */}
|
||||
<button
|
||||
onClick={onJumpToPreviousSentence}
|
||||
title="上一句 [Shift + ←]"
|
||||
className="p-2 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1.5 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<ChevronLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</button>
|
||||
|
||||
{/* Primary Play / Pause Button */}
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
title="播放 / 暂停 [空格键]"
|
||||
className="w-12 h-12 sm:w-14 sm:h-14 rounded-full bg-neutral-900 hover:bg-black dark:bg-neutral-100 dark:hover:bg-white text-white dark:text-black flex items-center justify-center shadow-lg active:scale-95 transition-all"
|
||||
className="w-10 h-10 sm:w-14 sm:h-14 short:w-9 short:h-9 rounded-full bg-neutral-900 hover:bg-black dark:bg-neutral-100 dark:hover:bg-white text-white dark:text-black flex items-center justify-center shadow-lg active:scale-95 transition-all"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<Pause className="w-5 h-5 fill-current" />
|
||||
<Pause className="w-4 h-4 sm:w-5 sm:h-5 fill-current" />
|
||||
) : (
|
||||
<Play className="w-5 h-5 fill-current ml-0.5" />
|
||||
<Play className="w-4 h-4 sm:w-5 sm:h-5 fill-current ml-0.5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -147,44 +147,44 @@ export const Controls: React.FC<ControlsProps> = ({
|
||||
<button
|
||||
onClick={onJumpToNextSentence}
|
||||
title="下一句 [Shift + →]"
|
||||
className="p-2 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1.5 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
<ChevronRight className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</button>
|
||||
|
||||
{/* Forward 10 */}
|
||||
<button
|
||||
onClick={() => onJumpBy(10)}
|
||||
title="前进 10 词 [→]"
|
||||
className="p-2 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1.5 sm:p-2.5 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronsRight className="w-5 h-5" />
|
||||
<ChevronsRight className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
</button>
|
||||
|
||||
{/* Quick Speed fine-tuning */}
|
||||
<div className="flex items-center space-x-1 pl-2 border-l border-neutral-300 dark:border-neutral-800">
|
||||
<div className="flex items-center space-x-1 pl-1.5 sm:pl-2 border-l border-neutral-300 dark:border-neutral-800">
|
||||
<button
|
||||
onClick={() => onChangeWpm(Math.max(100, settings.wpm - 25))}
|
||||
title="减速 25 WPM [↓]"
|
||||
className="p-1.5 rounded-md hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1 sm:p-1.5 rounded-md hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
<Minus className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
</button>
|
||||
<span className="text-xs font-mono font-semibold w-12 text-center text-neutral-700 dark:text-neutral-300">
|
||||
<span className="text-[11px] sm:text-xs font-mono font-semibold w-9 sm:w-12 text-center text-neutral-700 dark:text-neutral-300">
|
||||
{settings.wpm}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onChangeWpm(Math.min(1200, settings.wpm + 25))}
|
||||
title="加速 25 WPM [↑]"
|
||||
className="p-1.5 rounded-md hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
className="p-1 sm:p-1.5 rounded-md hover:bg-neutral-200 dark:hover:bg-neutral-800 text-neutral-500 hover:text-neutral-900 dark:hover:text-white transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<Plus className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preset WPM Chips */}
|
||||
<div className="flex items-center justify-center flex-wrap gap-1.5 pt-1 text-xs">
|
||||
{/* Preset WPM Chips (Hidden on short screens to prevent overflow) */}
|
||||
<div className="flex items-center justify-center flex-wrap gap-1.5 pt-0.5 sm:pt-1 text-xs short:hidden">
|
||||
<span className="text-neutral-400 text-[11px] mr-1 hidden sm:inline">
|
||||
预设语速:
|
||||
</span>
|
||||
|
||||
@@ -43,12 +43,12 @@ const ORP_COLOR_MAP: Record<OrpColor, { text: string; bg: string; glow: string }
|
||||
};
|
||||
|
||||
const FONT_SIZE_MAP: Record<FontSize, string> = {
|
||||
sm: 'text-3xl sm:text-4xl h-24 sm:h-28',
|
||||
base: 'text-4xl sm:text-5xl h-28 sm:h-32',
|
||||
lg: 'text-5xl sm:text-6xl h-32 sm:h-36',
|
||||
xl: 'text-6xl sm:text-7xl h-36 sm:h-44',
|
||||
'2xl': 'text-7xl sm:text-8xl h-44 sm:h-52',
|
||||
'3xl': 'text-8xl sm:text-9xl h-52 sm:h-64',
|
||||
sm: 'text-3xl sm:text-4xl h-24 sm:h-28 short:h-20 short:text-2xl',
|
||||
base: 'text-4xl sm:text-5xl h-28 sm:h-32 short:h-20 short:text-3xl',
|
||||
lg: 'text-5xl sm:text-6xl h-32 sm:h-36 short:h-24 short:text-3xl',
|
||||
xl: 'text-6xl sm:text-7xl h-36 sm:h-44 short:h-24 short:text-4xl',
|
||||
'2xl': 'text-7xl sm:text-8xl h-44 sm:h-52 short:h-24 short:text-4xl',
|
||||
'3xl': 'text-8xl sm:text-9xl h-52 sm:h-64 short:h-24 short:text-4xl',
|
||||
};
|
||||
|
||||
const FONT_FAMILY_MAP: Record<FontFamily, string> = {
|
||||
@@ -76,17 +76,17 @@ export const FocusDisplay: React.FC<FocusDisplayProps> = ({
|
||||
return (
|
||||
<div
|
||||
onClick={onTogglePlay}
|
||||
className="relative w-full max-w-4xl mx-auto flex flex-col items-center justify-center cursor-pointer select-none py-6 sm:py-10 transition-colors"
|
||||
className="relative w-full max-w-4xl mx-auto flex flex-col items-center justify-center cursor-pointer select-none py-2 sm:py-6 short:py-1 transition-colors flex-1 min-h-0"
|
||||
title="点击屏幕或按空格键 暂停/继续"
|
||||
>
|
||||
{/* Contextual Peripheral Ghost Words (Top line) */}
|
||||
{showContextPreview && (
|
||||
<div className="h-7 mb-3 flex items-center justify-center space-x-6 text-sm text-neutral-400 dark:text-neutral-500 opacity-60 transition-opacity">
|
||||
<span className="truncate max-w-[140px] text-right">
|
||||
<div className="h-5 sm:h-7 mb-1 sm:mb-2 short:hidden flex items-center justify-center space-x-4 sm:space-x-6 text-xs sm:text-sm text-neutral-400 dark:text-neutral-500 opacity-60 transition-opacity shrink-0">
|
||||
<span className="truncate max-w-[100px] sm:max-w-[140px] text-right">
|
||||
{previousToken ? previousToken.text : ''}
|
||||
</span>
|
||||
<span className="w-1 h-1 rounded-full bg-neutral-600/40" />
|
||||
<span className="truncate max-w-[140px] text-left">
|
||||
<span className="truncate max-w-[100px] sm:max-w-[140px] text-left">
|
||||
{nextToken ? nextToken.text : ''}
|
||||
</span>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@ export const FocusDisplay: React.FC<FocusDisplayProps> = ({
|
||||
|
||||
{/* Main RSVP Focal Chamber */}
|
||||
<div
|
||||
className={`relative w-full flex items-center justify-center border-y border-neutral-700/30 dark:border-neutral-800/60 bg-neutral-900/10 dark:bg-black/20 rounded-xl px-4 ${sizeClasses} ${familyClasses}`}
|
||||
className={`relative w-full flex items-center justify-center border-y border-neutral-700/30 dark:border-neutral-800/60 bg-neutral-900/10 dark:bg-black/20 rounded-xl px-2 sm:px-4 ${sizeClasses} ${familyClasses}`}
|
||||
>
|
||||
{/* Top & Bottom ORP Crosshair Guide Notches */}
|
||||
{showGuideLines && (
|
||||
@@ -153,7 +153,7 @@ export const FocusDisplay: React.FC<FocusDisplayProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Punctuation / Pause indicator dot */}
|
||||
<div className="h-4 mt-2 flex items-center justify-center">
|
||||
<div className="h-4 mt-1 sm:mt-2 short:hidden flex items-center justify-center shrink-0">
|
||||
{token?.isParagraphEnd && (
|
||||
<span className="text-[11px] font-mono tracking-widest text-neutral-400 dark:text-neutral-500 uppercase">
|
||||
¶ 段落停顿
|
||||
|
||||
@@ -40,7 +40,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
onCycleTheme,
|
||||
}) => {
|
||||
return (
|
||||
<header className="w-full flex items-center justify-between px-4 py-3 border-b border-neutral-200/50 dark:border-neutral-800/80 bg-white/50 dark:bg-black/30 backdrop-blur-md select-none">
|
||||
<header className="w-full flex items-center justify-between px-3 sm:px-4 py-1.5 sm:py-3 short:py-1 border-b border-neutral-200/50 dark:border-neutral-800/80 bg-white/50 dark:bg-black/30 backdrop-blur-md select-none shrink-0">
|
||||
{/* Left: App Brand & Current Book Info */}
|
||||
<div className="flex items-center space-x-3 truncate">
|
||||
{/* Logo */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Clipboard, Play, X } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, Clipboard, Play, X } from 'lucide-react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { tokenizeText } from '../core/tokenizer';
|
||||
import type { Book } from '../types';
|
||||
|
||||
@@ -19,22 +19,35 @@ export const PasteModal: React.FC<PasteModalProps> = ({
|
||||
const [content, setContent] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [isReadingClipboard, setIsReadingClipboard] = useState(false);
|
||||
const [clipboardError, setClipboardError] = useState<string | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handlePasteFromClipboard = async () => {
|
||||
try {
|
||||
setIsReadingClipboard(true);
|
||||
setClipboardError(null);
|
||||
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) {
|
||||
throw new Error('Clipboard API not supported');
|
||||
}
|
||||
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) {
|
||||
if (text && text.trim()) {
|
||||
setContent(text);
|
||||
if (!title) {
|
||||
const firstLine = text.trim().split('\n')[0].substring(0, 30);
|
||||
setTitle(firstLine || '剪贴板快读');
|
||||
}
|
||||
} else {
|
||||
setClipboardError('剪贴板中未检测到有效文本,请直接在下方文本框手动粘贴 (Ctrl+V)');
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Clipboard read failed', err);
|
||||
console.warn('Clipboard auto-read failed:', err);
|
||||
setClipboardError('无法自动读取剪贴板,请直接在文本框粘贴 (Ctrl+V)');
|
||||
textareaRef.current?.focus();
|
||||
} finally {
|
||||
setIsReadingClipboard(false);
|
||||
}
|
||||
@@ -67,6 +80,7 @@ export const PasteModal: React.FC<PasteModalProps> = ({
|
||||
onClose();
|
||||
setContent('');
|
||||
setTitle('');
|
||||
setClipboardError(null);
|
||||
};
|
||||
|
||||
const charCount = content.length;
|
||||
@@ -94,6 +108,21 @@ export const PasteModal: React.FC<PasteModalProps> = ({
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 space-y-4 flex-1 overflow-y-auto">
|
||||
{clipboardError && (
|
||||
<div className="p-3 rounded-xl bg-amber-500/10 border border-amber-500/25 text-amber-700 dark:text-amber-400 text-xs flex items-center justify-between animate-fade-in">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertCircle className="w-4 h-4 shrink-0 text-amber-500" />
|
||||
<span>{clipboardError}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setClipboardError(null)}
|
||||
className="text-amber-600 dark:text-amber-400 hover:opacity-75 text-sm font-bold ml-2"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-neutral-500 mb-1">
|
||||
标题(选填)
|
||||
@@ -122,10 +151,14 @@ export const PasteModal: React.FC<PasteModalProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={8}
|
||||
placeholder="在此粘贴任意中英文文本..."
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setContent(e.target.value);
|
||||
if (clipboardError) setClipboardError(null);
|
||||
}}
|
||||
className="w-full p-3 text-sm bg-neutral-100 dark:bg-neutral-800 rounded-lg border border-neutral-300 dark:border-neutral-700 focus:outline-none focus:border-rose-500 text-neutral-900 dark:text-neutral-100 resize-none font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -69,3 +69,73 @@ input[type="range"]::-moz-range-thumb {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 6px rgba(244, 63, 94, 0.4);
|
||||
}
|
||||
|
||||
/* Short / Landscape screen optimizations (< 520px viewport height) */
|
||||
@media (max-height: 520px) {
|
||||
.short\:py-0\.5 {
|
||||
padding-top: 0.125rem !important;
|
||||
padding-bottom: 0.125rem !important;
|
||||
}
|
||||
.short\:py-1 {
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0.25rem !important;
|
||||
}
|
||||
.short\:py-2 {
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.5rem !important;
|
||||
}
|
||||
.short\:pb-1 {
|
||||
padding-bottom: 0.25rem !important;
|
||||
}
|
||||
.short\:my-0 {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.short\:mb-1 {
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
.short\:mt-0\.5 {
|
||||
margin-top: 0.125rem !important;
|
||||
}
|
||||
.short\:h-20 {
|
||||
height: 5rem !important;
|
||||
}
|
||||
.short\:h-24 {
|
||||
height: 6rem !important;
|
||||
}
|
||||
.short\:h-5 {
|
||||
height: 1.25rem !important;
|
||||
}
|
||||
.short\:h-3 {
|
||||
height: 0.75rem !important;
|
||||
}
|
||||
.short\:text-2xl {
|
||||
font-size: 1.5rem !important;
|
||||
line-height: 2rem !important;
|
||||
}
|
||||
.short\:text-3xl {
|
||||
font-size: 1.875rem !important;
|
||||
line-height: 2.25rem !important;
|
||||
}
|
||||
.short\:text-4xl {
|
||||
font-size: 2.25rem !important;
|
||||
line-height: 2.5rem !important;
|
||||
}
|
||||
.short\:text-xs {
|
||||
font-size: 0.75rem !important;
|
||||
line-height: 1rem !important;
|
||||
}
|
||||
.short\:space-y-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: 0.25rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.short\:space-y-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.short\:hidden {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user