页面显示优化

This commit is contained in:
Dev
2026-11-02 17:15:39 +08:00
parent 061fa9d029
commit cf145c8266
15 changed files with 234 additions and 204 deletions
+3 -3
View File
@@ -475,7 +475,7 @@ const App: React.FC = () => {
return (
<ToastProvider>
<div className="flex flex-col min-h-screen bg-slate-50 dark:bg-slate-900 transition-colors duration-200">
<div className="flex flex-col min-h-screen bg-gradient-to-b from-slate-100 via-slate-50 to-white dark:from-slate-950 dark:via-slate-900 dark:to-slate-900 transition-colors duration-200">
<Header
user={currentUser}
onLogout={handleLogout}
@@ -488,8 +488,8 @@ const App: React.FC = () => {
darkMode={darkMode}
onToggleDarkMode={() => setDarkMode(!darkMode)}
/>
<main className="flex-grow container mx-auto px-3 sm:px-4 py-6 sm:py-8 md:py-12">
<div className="max-w-4xl mx-auto">
<main className="flex-grow container mx-auto px-4 sm:px-6 py-8 sm:py-10 md:py-14">
<div className="max-w-5xl mx-auto">
{renderContent()}
</div>
</main>
+2 -2
View File
@@ -93,13 +93,13 @@ const AddTaskModal: React.FC<AddTaskModalProps> = ({ mode, parentTask, siblingTa
return (
<div
className="fixed inset-0 bg-slate-900/50 dark:bg-slate-900/70 flex items-center justify-center z-50 p-4"
className="fixed inset-0 bg-slate-900/40 dark:bg-slate-950/70 backdrop-blur-sm flex items-center justify-center z-50 p-4"
aria-modal="true"
role="dialog"
onClick={onCancel}
>
<div
className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-lg p-6 md:p-8 relative max-h-[90vh] overflow-y-auto"
className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-lg p-6 md:p-8 relative max-h-[90vh] overflow-y-auto border border-slate-200/80 dark:border-slate-700"
onClick={(e) => e.stopPropagation()}
>
<button
+2 -2
View File
@@ -76,13 +76,13 @@ const EditTaskModal: React.FC<EditTaskModalProps> = ({ task, allTasks, onSave, o
return (
<div
className="fixed inset-0 bg-slate-900/50 dark:bg-slate-900/70 flex items-center justify-center z-50 p-4"
className="fixed inset-0 bg-slate-900/40 dark:bg-slate-950/70 backdrop-blur-sm flex items-center justify-center z-50 p-4"
aria-modal="true"
role="dialog"
onClick={onCancel}
>
<div
className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-lg p-6 md:p-8 relative max-h-[90vh] overflow-y-auto"
className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-lg p-6 md:p-8 relative max-h-[90vh] overflow-y-auto border border-slate-200/80 dark:border-slate-700"
onClick={(e) => e.stopPropagation()}
>
<button
+1 -1
View File
@@ -4,7 +4,7 @@ import { useLanguage } from '../i18n/LanguageContext';
const Footer: React.FC = () => {
const { t } = useLanguage();
return (
<footer className="bg-slate-100 dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700">
<footer className="bg-white/70 dark:bg-slate-900/80 border-t border-slate-200/80 dark:border-slate-700">
<div className="container mx-auto px-4 py-6 text-center text-slate-500 dark:text-slate-400">
<p>{t('rights', { year: new Date().getFullYear() })}</p>
<p className="text-sm mt-1">{t('powered')}</p>
+9 -9
View File
@@ -24,15 +24,15 @@ const GoalInputForm: React.FC<GoalInputFormProps> = ({ onGeneratePlan }) => {
};
return (
<div className="bg-white dark:bg-slate-800 p-8 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700 text-center">
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2">{t('nextGoalTitle')}</h2>
<p className="text-slate-600 dark:text-slate-400 mb-6">{t('nextGoalSub')}</p>
<div className="bg-white dark:bg-slate-800 p-6 sm:p-10 rounded-3xl shadow-sm border border-slate-200/80 dark:border-slate-700 text-center">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-800 dark:text-slate-100 mb-2">{t('nextGoalTitle')}</h2>
<p className="text-slate-600 dark:text-slate-400 mb-7">{t('nextGoalSub')}</p>
<form onSubmit={handleSubmit}>
<textarea
value={goal}
onChange={(e) => setGoal(e.target.value)}
placeholder={t('goalPlaceholder')}
className="w-full p-4 bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-600 rounded-lg mb-2 focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow text-lg placeholder:text-slate-400 dark:placeholder:text-slate-500"
className="w-full p-4 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-2xl mb-3 focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:bg-white dark:focus:bg-slate-700 transition-shadow text-lg placeholder:text-slate-400 dark:placeholder:text-slate-500"
rows={3}
/>
@@ -54,7 +54,7 @@ const GoalInputForm: React.FC<GoalInputFormProps> = ({ onGeneratePlan }) => {
value={additionalInfo}
onChange={(e) => setAdditionalInfo(e.target.value)}
placeholder={t('importPlaceholder')}
className="w-full p-4 bg-slate-50 dark:bg-slate-700/50 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-500 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow text-sm placeholder:text-slate-400 dark:placeholder:text-slate-500 font-mono"
className="w-full p-4 bg-slate-50 dark:bg-slate-700/50 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-500 rounded-2xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-shadow text-sm placeholder:text-slate-400 dark:placeholder:text-slate-500 font-mono"
rows={8}
/>
{additionalInfo.trim() && (
@@ -68,7 +68,7 @@ const GoalInputForm: React.FC<GoalInputFormProps> = ({ onGeneratePlan }) => {
<button
type="submit"
disabled={!goal.trim()}
className="w-full bg-green-600 text-white font-semibold px-6 py-3 rounded-lg hover:bg-green-700 transition-colors disabled:bg-slate-400 dark:disabled:bg-slate-600 disabled:cursor-not-allowed"
className="w-full bg-emerald-600 text-white font-semibold px-6 py-3.5 rounded-xl hover:bg-emerald-700 transition-colors disabled:bg-slate-300 dark:disabled:bg-slate-600 disabled:cursor-not-allowed shadow-sm shadow-emerald-200 dark:shadow-none"
>
{t('generatePlan')}
</button>
@@ -76,9 +76,9 @@ const GoalInputForm: React.FC<GoalInputFormProps> = ({ onGeneratePlan }) => {
<div className="mt-6">
<p className="text-sm text-slate-500 dark:text-slate-400">{t('orTryExample')}</p>
<div className="flex flex-wrap justify-center gap-2 mt-2">
<button onClick={() => handleExampleClick(t('exampleTripGoal'))} className="text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 px-3 py-1 rounded-full hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">{t('exampleTrip')}</button>
<button onClick={() => handleExampleClick(t('exampleSkillGoal'))} className="text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 px-3 py-1 rounded-full hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">{t('exampleSkill')}</button>
<button onClick={() => handleExampleClick(t('exampleProductGoal'))} className="text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 px-3 py-1 rounded-full hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors">{t('exampleProduct')}</button>
<button onClick={() => handleExampleClick(t('exampleTripGoal'))} className="text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 px-3.5 py-1.5 rounded-full hover:bg-emerald-50 hover:text-emerald-700 dark:hover:bg-emerald-900/30 dark:hover:text-emerald-300 transition-colors">{t('exampleTrip')}</button>
<button onClick={() => handleExampleClick(t('exampleSkillGoal'))} className="text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 px-3.5 py-1.5 rounded-full hover:bg-emerald-50 hover:text-emerald-700 dark:hover:bg-emerald-900/30 dark:hover:text-emerald-300 transition-colors">{t('exampleSkill')}</button>
<button onClick={() => handleExampleClick(t('exampleProductGoal'))} className="text-xs bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 px-3.5 py-1.5 rounded-full hover:bg-emerald-50 hover:text-emerald-700 dark:hover:bg-emerald-900/30 dark:hover:text-emerald-300 transition-colors">{t('exampleProduct')}</button>
</div>
</div>
</div>
+1 -1
View File
@@ -78,7 +78,7 @@ const Header: React.FC<HeaderProps> = ({ user, onLogout, onLoginClick, onSignUpC
}, [showMobileMenu]);
return (
<header className="bg-white dark:bg-slate-800 shadow-sm border-b border-slate-200 dark:border-slate-700 sticky top-0 z-40">
<header className="bg-white/85 dark:bg-slate-900/85 backdrop-blur-md shadow-sm border-b border-slate-200/80 dark:border-slate-700/80 sticky top-0 z-40">
<div className="container mx-auto px-3 sm:px-4 py-2.5 sm:py-3 flex justify-between items-center">
<div
className="flex items-center gap-2 cursor-pointer"
+16 -15
View File
@@ -10,8 +10,8 @@ const GenerateIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" v
const ExecuteIcon = () => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6"><path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>;
const FeatureCard: React.FC<{ icon: React.ReactElement; title: string; children: React.ReactNode }> = ({ icon, title, children }) => (
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md border border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-center h-12 w-12 rounded-full bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400 mb-4">
<div className="bg-white dark:bg-slate-800 p-6 sm:p-7 rounded-2xl shadow-sm border border-slate-200/80 dark:border-slate-700 text-left hover:-translate-y-0.5 hover:shadow-md transition-all duration-200">
<div className="flex items-center justify-center h-12 w-12 rounded-2xl bg-emerald-50 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 mb-4">
{icon}
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">{title}</h3>
@@ -24,26 +24,27 @@ const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted }) => {
return (
<div className="text-center">
{/* Hero Section */}
<section className="py-16 md:py-24">
<h1 className="text-4xl md:text-6xl font-extrabold text-slate-900 dark:text-slate-100 mb-4 tracking-tight">
<section className="py-14 md:py-20">
<p className="inline-flex items-center rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 text-xs font-semibold px-3 py-1 mb-5">Goal Planner</p>
<h1 className="text-4xl md:text-6xl font-extrabold text-slate-900 dark:text-slate-100 mb-5 tracking-tight leading-[1.1]">
{t('heroTitle')}
</h1>
<p className="max-w-2xl mx-auto text-lg md:text-xl text-slate-600 dark:text-slate-400 mb-8">
<p className="max-w-2xl mx-auto text-lg md:text-xl text-slate-600 dark:text-slate-400 mb-8 leading-relaxed">
{t('heroSubtitle')}
</p>
<button
onClick={onGetStarted}
className="bg-green-600 text-white font-semibold px-8 py-3 rounded-lg hover:bg-green-700 transition-colors shadow-lg transform hover:scale-105"
className="bg-emerald-600 text-white font-semibold px-8 py-3.5 rounded-xl hover:bg-emerald-700 transition-all shadow-lg shadow-emerald-200/70 dark:shadow-none hover:-translate-y-0.5"
>
{t('createFirstPlan')}
</button>
</section>
{/* Features Section */}
<section id="how-it-works" className="py-16 md:py-24 bg-slate-100 dark:bg-slate-800/50 -mx-4 px-4 sm:rounded-2xl">
<section id="how-it-works" className="py-14 md:py-20 bg-white/70 dark:bg-slate-800/40 -mx-4 px-4 sm:mx-0 sm:px-8 sm:rounded-3xl border border-slate-200/70 dark:border-slate-700/60">
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-2">{t('howItWorks')}</h2>
<p className="max-w-xl mx-auto text-slate-600 dark:text-slate-400 mb-12">{t('howItWorksSub')}</p>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
<div className="grid md:grid-cols-3 gap-5 max-w-5xl mx-auto text-left">
<FeatureCard
icon={<GoalIcon />}
title={t('step1Title')}
@@ -69,12 +70,12 @@ const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted }) => {
<section id="testimonials" className="py-16 md:py-24">
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-12">{t('testimonialsTitle')}</h2>
<div className="max-w-4xl mx-auto grid md:grid-cols-2 gap-8">
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md border border-slate-200 dark:border-slate-700 text-left">
<div className="bg-white dark:bg-slate-800 p-6 sm:p-7 rounded-2xl shadow-sm border border-slate-200/80 dark:border-slate-700 text-left">
<p className="text-slate-600 dark:text-slate-400 mb-4">{t('testimonial1')}</p>
<p className="font-semibold text-slate-900 dark:text-slate-100">Goal Planner</p>
<p className="text-sm text-slate-500 dark:text-slate-500">{t('testimonial1Role')}</p>
</div>
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-md border border-slate-200 dark:border-slate-700 text-left">
<div className="bg-white dark:bg-slate-800 p-6 sm:p-7 rounded-2xl shadow-sm border border-slate-200/80 dark:border-slate-700 text-left">
<p className="text-slate-600 dark:text-slate-400 mb-4">{t('testimonial2')}</p>
<p className="font-semibold text-slate-900 dark:text-slate-100">Balaji</p>
<p className="text-sm text-slate-500 dark:text-slate-500">{t('testimonial2Role')}</p>
@@ -83,7 +84,7 @@ const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted }) => {
</section>
{/* About Section */}
<section id="about" className="py-16 md:py-24 bg-slate-100 dark:bg-slate-800/50 -mx-4 px-4 sm:rounded-2xl">
<section id="about" className="py-14 md:py-20 bg-white/70 dark:bg-slate-800/40 -mx-4 px-4 sm:mx-0 sm:px-8 sm:rounded-3xl border border-slate-200/70 dark:border-slate-700/60">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl font-bold text-slate-900 dark:text-slate-100 mb-4">{t('missionTitle')}</h2>
<p className="text-lg text-slate-600 dark:text-slate-400 mb-8">
@@ -94,14 +95,14 @@ const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted }) => {
{/* Final CTA */}
<section className="py-16 md:py-24">
<div className="max-w-4xl mx-auto text-center bg-green-600 dark:bg-green-700 text-white -mx-4 py-16 px-4 sm:rounded-2xl">
<h2 className="text-4xl font-extrabold mb-4">{t('ctaTitle')}</h2>
<p className="text-lg text-green-100 mb-8 max-w-2xl mx-auto">
<div className="max-w-4xl mx-auto text-center bg-gradient-to-br from-emerald-600 to-teal-600 dark:from-emerald-700 dark:to-teal-800 text-white py-14 sm:py-16 px-6 sm:px-10 rounded-3xl shadow-lg shadow-emerald-200/50 dark:shadow-none">
<h2 className="text-3xl sm:text-4xl font-extrabold mb-4">{t('ctaTitle')}</h2>
<p className="text-lg text-emerald-50 mb-8 max-w-2xl mx-auto">
{t('ctaBody')}
</p>
<button
onClick={onGetStarted}
className="bg-white dark:bg-slate-900 text-green-600 dark:text-green-400 font-bold px-10 py-4 rounded-lg hover:bg-green-50 dark:hover:bg-slate-800 transition-colors shadow-2xl transform hover:scale-105 text-lg"
className="bg-white text-emerald-700 font-bold px-10 py-4 rounded-xl hover:bg-emerald-50 transition-all shadow-xl hover:-translate-y-0.5 text-lg"
>
{t('ctaButton')}
</button>
+4 -4
View File
@@ -25,7 +25,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, onNavigateToSignUp }) =>
};
return (
<div className="bg-white dark:bg-slate-800 p-8 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700 w-full max-w-md">
<div className="bg-white dark:bg-slate-800 p-8 sm:p-10 rounded-3xl shadow-sm border border-slate-200/80 dark:border-slate-700 w-full max-w-md mx-auto">
<AppIcon />
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2 text-center">{t('welcomeTitle')}</h2>
<p className="text-slate-600 dark:text-slate-400 mb-6 text-center">{t('loginSubtitle')}</p>
@@ -37,7 +37,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, onNavigateToSignUp }) =>
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-3 bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
className="w-full p-3 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:bg-white dark:focus:bg-slate-700 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="you@example.com"
required
/>
@@ -49,14 +49,14 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLogin, onNavigateToSignUp }) =>
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
className="w-full p-3 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:bg-white dark:focus:bg-slate-700 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
className="w-full bg-green-600 text-white font-semibold px-6 py-3 rounded-lg hover:bg-green-700 transition-colors disabled:bg-slate-400 dark:disabled:bg-slate-600 disabled:cursor-not-allowed"
className="w-full bg-emerald-600 text-white font-semibold px-6 py-3 rounded-xl hover:bg-emerald-700 transition-colors disabled:bg-slate-300 dark:disabled:bg-slate-600 disabled:cursor-not-allowed"
disabled={!email || !password}
>
{t('login')}
+4 -4
View File
@@ -118,7 +118,7 @@ const PlanHistory: React.FC<PlanHistoryProps> = ({ onViewPlan, onCreateNew }) =>
</div>
<button
onClick={onCreateNew}
className="bg-green-600 text-white font-semibold px-4 sm:px-5 py-2 sm:py-2.5 rounded-lg hover:bg-green-700 transition-colors text-sm flex items-center gap-2"
className="bg-emerald-600 text-white font-semibold px-4 sm:px-5 py-2 sm:py-2.5 rounded-xl hover:bg-emerald-700 transition-colors text-sm flex items-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@@ -134,7 +134,7 @@ const PlanHistory: React.FC<PlanHistoryProps> = ({ onViewPlan, onCreateNew }) =>
)}
{plans.length === 0 && !error ? (
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-12 text-center">
<div className="bg-white dark:bg-slate-800 rounded-3xl shadow-sm border border-slate-200/80 dark:border-slate-700 p-12 text-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 text-slate-300 dark:text-slate-600 mx-auto mb-4" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
@@ -142,7 +142,7 @@ const PlanHistory: React.FC<PlanHistoryProps> = ({ onViewPlan, onCreateNew }) =>
<p className="text-slate-500 dark:text-slate-400 mb-6">{t('emptyPlansSub')}</p>
<button
onClick={onCreateNew}
className="bg-green-600 text-white font-semibold px-6 py-2.5 rounded-lg hover:bg-green-700 transition-colors"
className="bg-emerald-600 text-white font-semibold px-6 py-2.5 rounded-xl hover:bg-emerald-700 transition-colors"
>
{t('createFirst')}
</button>
@@ -155,7 +155,7 @@ const PlanHistory: React.FC<PlanHistoryProps> = ({ onViewPlan, onCreateNew }) =>
<div
key={plan.id}
onClick={() => handleViewPlan(plan.id)}
className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-4 sm:p-5 hover:shadow-md hover:border-green-300 dark:hover:border-green-600 transition-all cursor-pointer group"
className="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200/80 dark:border-slate-700 p-5 sm:p-6 hover:shadow-md hover:border-emerald-300 dark:hover:border-emerald-600 transition-all cursor-pointer group"
>
<div className="flex justify-between items-start">
<div className="flex-1 min-w-0">
+19 -18
View File
@@ -329,17 +329,18 @@ const PlannerView: React.FC<PlannerViewProps> = ({ goal, tasks, planId, onToggle
return (
<div className="space-y-6">
<div className="bg-white dark:bg-slate-800 p-4 sm:p-6 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700">
<div className="flex flex-col sm:flex-row justify-between items-start gap-3">
<div className="relative overflow-hidden bg-white dark:bg-slate-800 p-5 sm:p-7 rounded-2xl shadow-sm border border-slate-200/80 dark:border-slate-700">
<div className="pointer-events-none absolute -right-16 -top-16 h-40 w-40 rounded-full bg-emerald-100/70 dark:bg-emerald-900/20 blur-2xl" />
<div className="relative flex flex-col sm:flex-row justify-between items-start gap-4">
<div className="min-w-0">
<p className="text-sm font-medium text-slate-500 dark:text-slate-400">{t('yourGoal')}</p>
<h2 className="text-xl sm:text-2xl font-bold text-slate-800 dark:text-slate-100 mt-1 break-words">{goal}</h2>
<p className="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">{t('yourGoal')}</p>
<h2 className="text-xl sm:text-2xl font-bold text-slate-800 dark:text-slate-100 mt-1.5 break-words leading-snug">{goal}</h2>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{onMyPlans && (
<button
onClick={onMyPlans}
className="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold px-3 sm:px-4 py-2 rounded-lg hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors text-sm flex items-center gap-1.5"
className="bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-200 font-semibold px-3 sm:px-4 py-2.5 rounded-xl hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors text-sm flex items-center gap-1.5"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />
@@ -349,29 +350,29 @@ const PlannerView: React.FC<PlannerViewProps> = ({ goal, tasks, planId, onToggle
)}
<button
onClick={onStartOver}
className="bg-green-600 text-white font-semibold px-3 sm:px-4 py-2 rounded-lg hover:bg-green-700 transition-colors text-sm"
className="bg-emerald-600 text-white font-semibold px-3 sm:px-4 py-2.5 rounded-xl hover:bg-emerald-700 transition-colors text-sm shadow-sm shadow-emerald-200 dark:shadow-none"
>
{t('newPlan')}
</button>
</div>
</div>
<div className="mt-6">
<div className="flex justify-between items-center mb-1">
<div className="relative mt-6">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-slate-600 dark:text-slate-300">{t('progress')}</span>
<span className="text-sm font-bold text-green-600 dark:text-green-400">{t('tasksCount', { done: completedTasks, total: totalTasks })}</span>
<span className="text-sm font-bold text-emerald-600 dark:text-emerald-400">{t('tasksCount', { done: completedTasks, total: totalTasks })}</span>
</div>
<div className="w-full bg-slate-200 dark:bg-slate-600 rounded-full h-2.5">
<div className="bg-green-600 dark:bg-green-500 h-2.5 rounded-full transition-all duration-500" style={{ width: `${progress}%` }}></div>
<div className="w-full bg-slate-100 dark:bg-slate-700 rounded-full h-2.5">
<div className="bg-gradient-to-r from-emerald-500 to-teal-400 h-2.5 rounded-full transition-all duration-500" style={{ width: `${progress}%` }}></div>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2 text-right">
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2.5 text-right">
{t('totalTime', { hours: totalDuration.toFixed(1) })}
</p>
</div>
</div>
{/* Search & Filter Bar */}
<div className="bg-white dark:bg-slate-800 p-3 sm:p-4 rounded-xl shadow border border-slate-200 dark:border-slate-700">
<div className="bg-white dark:bg-slate-800 p-3 sm:p-4 rounded-2xl shadow-sm border border-slate-200/80 dark:border-slate-700">
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
{/* Search */}
<div className="relative flex-grow">
@@ -383,14 +384,14 @@ const PlannerView: React.FC<PlannerViewProps> = ({ goal, tasks, planId, onToggle
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('searchTasks')}
className="w-full pl-9 pr-3 py-2 text-sm bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow"
className="w-full pl-9 pr-3 py-2.5 text-sm bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-shadow"
/>
</div>
{/* Status filter */}
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as 'all' | TaskStatus)}
className="px-3 py-2 text-sm bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500"
className="px-3 py-2.5 text-sm bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500"
>
<option value="all">{t('statusAll')}</option>
<option value="todo">{t('statusTodo')}</option>
@@ -401,7 +402,7 @@ const PlannerView: React.FC<PlannerViewProps> = ({ goal, tasks, planId, onToggle
<select
value={filterPriority}
onChange={(e) => setFilterPriority(e.target.value as 'all' | 'low' | 'medium' | 'high')}
className="px-3 py-2 text-sm bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500"
className="px-3 py-2.5 text-sm bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-200 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500"
>
<option value="all">{t('priorityAll')}</option>
<option value="high">{t('priorityHighFull')}</option>
@@ -412,9 +413,9 @@ const PlannerView: React.FC<PlannerViewProps> = ({ goal, tasks, planId, onToggle
</div>
<div>
<h3 className="text-xl font-bold text-slate-800 dark:text-slate-100 mb-4">{t('actionPlan')}</h3>
<h3 className="text-lg sm:text-xl font-bold text-slate-800 dark:text-slate-100 mb-4">{t('actionPlan')}</h3>
{filteredTasks.length === 0 ? (
<div className="text-center py-12 text-slate-400 dark:text-slate-500">
<div className="text-center py-16 text-slate-400 dark:text-slate-500 bg-white dark:bg-slate-800 rounded-2xl border border-dashed border-slate-200 dark:border-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12 mx-auto mb-3 opacity-50" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
+5 -5
View File
@@ -26,7 +26,7 @@ const SignUpPage: React.FC<SignUpPageProps> = ({ onSignUp, onNavigateToLogin })
};
return (
<div className="bg-white dark:bg-slate-800 p-8 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700 w-full max-w-md">
<div className="bg-white dark:bg-slate-800 p-8 sm:p-10 rounded-3xl shadow-sm border border-slate-200/80 dark:border-slate-700 w-full max-w-md mx-auto">
<AppIcon />
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-2 text-center">{t('createAccount')}</h2>
<p className="text-slate-600 dark:text-slate-400 mb-6 text-center">{t('signupSubtitle')}</p>
@@ -38,7 +38,7 @@ const SignUpPage: React.FC<SignUpPageProps> = ({ onSignUp, onNavigateToLogin })
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full p-3 bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
className="w-full p-3 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:bg-white dark:focus:bg-slate-700 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder={t('namePlaceholder')}
required
/>
@@ -50,7 +50,7 @@ const SignUpPage: React.FC<SignUpPageProps> = ({ onSignUp, onNavigateToLogin })
id="signup-email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-3 bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
className="w-full p-3 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:bg-white dark:focus:bg-slate-700 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="you@example.com"
required
/>
@@ -62,14 +62,14 @@ const SignUpPage: React.FC<SignUpPageProps> = ({ onSignUp, onNavigateToLogin })
id="signup-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
className="w-full p-3 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-slate-100 border border-slate-200 dark:border-slate-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:bg-white dark:focus:bg-slate-700 transition-shadow placeholder:text-slate-400 dark:placeholder:text-slate-500"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
className="w-full bg-green-600 text-white font-semibold px-6 py-3 rounded-lg hover:bg-green-700 transition-colors disabled:bg-slate-400 dark:disabled:bg-slate-600 disabled:cursor-not-allowed"
className="w-full bg-emerald-600 text-white font-semibold px-6 py-3 rounded-xl hover:bg-emerald-700 transition-colors disabled:bg-slate-300 dark:disabled:bg-slate-600 disabled:cursor-not-allowed"
disabled={!name || !email || !password}
>
{t('createAccount')}
+165 -137
View File
@@ -31,9 +31,39 @@ const ClockIcon = () => (
);
const levelConfig = [
{ accent: 'emerald', badge: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300', border: 'border-l-emerald-400', hoverBg: 'hover:border-l-emerald-500', barColor: 'bg-emerald-500', barTrack: 'bg-emerald-100 dark:bg-emerald-900/30', iconBg: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400', checkBg: 'bg-emerald-600 border-emerald-600' },
{ accent: 'sky', badge: 'bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300', border: 'border-l-sky-400', hoverBg: 'hover:border-l-sky-500', barColor: 'bg-sky-500', barTrack: 'bg-sky-100 dark:bg-sky-900/30', iconBg: 'bg-sky-50 text-sky-600 dark:bg-sky-900/30 dark:text-sky-400', checkBg: 'bg-sky-600 border-sky-600' },
{ accent: 'violet', badge: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300', border: 'border-l-violet-400', hoverBg: 'hover:border-l-violet-500', barColor: 'bg-violet-500', barTrack: 'bg-violet-100 dark:bg-violet-900/30', iconBg: 'bg-violet-50 text-violet-600 dark:bg-violet-900/30 dark:text-violet-400', checkBg: 'bg-violet-600 border-violet-600' },
{
accent: 'emerald',
badge: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
border: 'border-l-emerald-500',
barColor: 'bg-emerald-500',
barTrack: 'bg-emerald-100 dark:bg-emerald-900/40',
iconBg: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-900/40 dark:text-emerald-300',
checkBg: 'bg-emerald-600 border-emerald-600',
nest: 'bg-emerald-50/80 dark:bg-emerald-950/30 border-emerald-100 dark:border-emerald-900/50',
rail: 'bg-emerald-300 dark:bg-emerald-700',
},
{
accent: 'sky',
badge: 'bg-sky-100 text-sky-700 dark:bg-sky-900/40 dark:text-sky-300',
border: 'border-l-sky-500',
barColor: 'bg-sky-500',
barTrack: 'bg-sky-100 dark:bg-sky-900/40',
iconBg: 'bg-sky-50 text-sky-600 dark:bg-sky-900/40 dark:text-sky-300',
checkBg: 'bg-sky-600 border-sky-600',
nest: 'bg-sky-50/80 dark:bg-sky-950/25 border-sky-100 dark:border-sky-900/40',
rail: 'bg-sky-300 dark:bg-sky-700',
},
{
accent: 'violet',
badge: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300',
border: 'border-l-violet-500',
barColor: 'bg-violet-500',
barTrack: 'bg-violet-100 dark:bg-violet-900/40',
iconBg: 'bg-violet-50 text-violet-600 dark:bg-violet-900/40 dark:text-violet-300',
checkBg: 'bg-violet-600 border-violet-600',
nest: 'bg-violet-50/80 dark:bg-violet-950/25 border-violet-100 dark:border-violet-900/40',
rail: 'bg-violet-300 dark:bg-violet-700',
},
];
const priorityConfig: Record<TaskPriority, { color: string; dot: string }> = {
@@ -124,56 +154,97 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
}
}, [task.dueDate, task.status, t]);
const indentPx = level * 28;
const cardPad = level === 0 ? 'px-3.5 py-3.5 sm:px-5 sm:py-5' : 'px-3.5 py-3 sm:px-4 sm:py-3.5';
const titleSize = level === 0 ? 'text-base sm:text-[17px]' : level === 1 ? 'text-[15px]' : 'text-sm';
const actionButtons = (
<>
<button
onClick={(e) => { e.stopPropagation(); onAddSiblingTask(task); }}
className="p-1.5 sm:p-2 text-teal-500 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-300 rounded-lg hover:bg-teal-50 dark:hover:bg-teal-900/20 transition-all duration-150"
title={t('addSibling')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 2a.75.75 0 01.75.75v6.5h2.94l-1.47-1.47a.75.75 0 111.06-1.06l2.75 2.75a.75.75 0 010 1.06l-2.75 2.75a.75.75 0 11-1.06-1.06l1.47-1.47h-2.94v6.5a.75.75 0 01-1.5 0v-6.5H6.31l1.47 1.47a.75.75 0 11-1.06 1.06l-2.75-2.75a.75.75 0 010-1.06l2.75-2.75a.75.75 0 011.06 1.06L6.31 9.25h2.94v-6.5A.75.75 0 0110 2z" clipRule="evenodd" />
</svg>
</button>
{level < 2 && (
<button
onClick={(e) => { e.stopPropagation(); onAddChildTask(task); }}
className="p-1.5 sm:p-2 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all duration-150"
title={t('addChild')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M2 4.75A.75.75 0 012.75 4h3a.75.75 0 010 1.5H4.5v7.75a2.25 2.25 0 002.25 2.25h4.19l-1.47-1.47a.75.75 0 111.06-1.06l2.75 2.75a.75.75 0 010 1.06l-2.75 2.75a.75.75 0 11-1.06-1.06l1.47-1.47H6.75A3.75 3.75 0 013 12.25V5.5H2.75A.75.75 0 012 4.75z" clipRule="evenodd" />
</svg>
</button>
)}
<button
onClick={(e) => { e.stopPropagation(); onEditTask(task); }}
className="p-1.5 sm:p-2 text-emerald-500 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 rounded-lg hover:bg-emerald-50 dark:hover:bg-emerald-900/20 transition-all duration-150"
title={t('editTask')}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
</svg>
</button>
<button
onClick={(e) => { e.stopPropagation(); setShowDeleteConfirm(true); }}
className="p-1.5 sm:p-2 text-red-400 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-all duration-150"
title={t('deleteTask')}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4">
<path fillRule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022 1.005 11.27A2.75 2.75 0 007.77 19h4.46a2.75 2.75 0 002.751-2.529l1.005-11.27.149.022a.75.75 0 10.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clipRule="evenodd" />
</svg>
</button>
</>
);
return (
<div
className={`group/task ${dragOverTask?.id === task.id ? 'ring-2 ring-blue-400/50 rounded-lg' : ''}`}
className={`group/task ${dragOverTask?.id === task.id ? 'ring-2 ring-blue-400/60 rounded-2xl' : ''}`}
draggable
onDragStart={(e) => { e.stopPropagation(); onDragStart?.(task); }}
onDragOver={(e) => { e.stopPropagation(); onDragOver?.(e, task); }}
onDrop={(e) => { e.stopPropagation(); onDrop?.(e, task); }}
onDragEnd={() => { onDragEnd?.(); }}
>
{/* Main task row */}
<div
style={{ marginLeft: indentPx }}
className={`
relative bg-white dark:bg-slate-800 rounded-lg border border-l-[3px] ${config.border} ${config.hoverBg}
border-slate-200/80 dark:border-slate-600/50
px-3.5 py-3
relative bg-white dark:bg-slate-800 border-l-4 ${config.border}
border border-slate-200/90 dark:border-slate-700
${level === 0 ? 'rounded-2xl shadow-sm' : 'rounded-xl'}
${cardPad}
transition-all duration-200 ease-out
${isDone ? 'bg-slate-50/70 dark:bg-slate-800/70' : isInProgress ? 'bg-blue-50/30 dark:bg-blue-900/10' : isLocked ? 'bg-slate-50/50 dark:bg-slate-800/50' : 'hover:shadow-md hover:shadow-slate-100 dark:hover:shadow-slate-900/30'}
${isDone ? 'border-slate-200/50 dark:border-slate-600/30' : ''}
${isDone ? 'bg-slate-50/80 dark:bg-slate-800/70' : isInProgress ? 'bg-blue-50/40 dark:bg-blue-950/20' : isLocked ? 'bg-slate-50/70 dark:bg-slate-800/60' : 'hover:shadow-md hover:shadow-slate-200/70 dark:hover:shadow-black/20'}
`}
>
<div className="flex items-start gap-2.5">
{/* Collapse / dot indicator */}
<div className="flex items-start gap-2 sm:gap-3">
{hasChildren ? (
<button
onClick={() => setCollapsed(!collapsed)}
className={`flex-shrink-0 mt-0.5 w-5 h-5 rounded-md flex items-center justify-center ${config.iconBg} hover:scale-110 active:scale-95 transition-all duration-150`}
className={`flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 rounded-lg flex items-center justify-center ${config.iconBg} hover:scale-105 active:scale-95 transition-all duration-150`}
title={collapsed ? t('expand') : t('collapse')}
aria-expanded={!collapsed}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-3 w-3 transition-transform duration-200 ${collapsed ? '' : 'rotate-90'}`}
className={`h-4 w-4 transition-transform duration-200 ${collapsed ? '' : 'rotate-90'}`}
fill="none" viewBox="0 0 24 24" strokeWidth={2.5} stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
) : (
<div className="flex-shrink-0 mt-1 w-5 h-5 flex items-center justify-center">
<div className={`w-1.5 h-1.5 rounded-full transition-colors duration-200 ${isDone ? 'bg-emerald-400' : isInProgress ? 'bg-blue-400 animate-pulse' : 'bg-slate-300 dark:bg-slate-500'}`} />
<div className="flex-shrink-0 w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center">
<div className={`w-2 h-2 rounded-full transition-colors duration-200 ${isDone ? 'bg-emerald-400' : isInProgress ? 'bg-blue-400 animate-pulse' : 'bg-slate-300 dark:bg-slate-500'}`} />
</div>
)}
{/* Checkbox / Status toggle */}
<div className="flex-shrink-0 mt-0.5 relative" ref={statusMenuRef}>
<div className="flex-shrink-0 mt-1 relative" ref={statusMenuRef}>
{hasChildren && isLocked && !isDone ? (
<div title={t('depLocked')} className="w-[18px] h-[18px] flex items-center justify-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-[18px] h-[18px]">
<div title={t('depLocked')} className="w-5 h-5 flex items-center justify-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5">
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clipRule="evenodd" />
</svg>
</div>
@@ -185,17 +256,17 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
tabIndex={0}
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') handleToggle() }}
title={isDone ? t('autoDone') : t('finishChildrenFirst')}
className={`w-[18px] h-[18px] rounded-[5px] border-2 flex items-center justify-center transition-all duration-200 ${isDone ? `${config.checkBg}` : 'bg-slate-50 dark:bg-slate-700 border-slate-300 dark:border-slate-500'} cursor-pointer active:scale-90`}
className={`w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all duration-200 ${isDone ? `${config.checkBg}` : 'bg-slate-50 dark:bg-slate-700 border-slate-300 dark:border-slate-500'} cursor-pointer active:scale-90`}
>
{isDone && (
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3.5}>
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
</div>
) : effectivelyLocked && !isDone ? (
<div title={t('needDeps')} className="w-[18px] h-[18px] flex items-center justify-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-[18px] h-[18px]">
<div title={t('needDeps')} className="w-5 h-5 flex items-center justify-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5">
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clipRule="evenodd" />
</svg>
</div>
@@ -213,13 +284,13 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
}
}}
className={`
w-[18px] h-[18px] rounded-[5px] border-2 flex items-center justify-center
w-5 h-5 rounded-md border-2 flex items-center justify-center
transition-all duration-200 cursor-pointer active:scale-90
${isDone ? `${config.checkBg}` : isInProgress ? 'bg-blue-500 border-blue-500' : 'bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-500 hover:border-emerald-400'}
`}
>
{isDone && (
<svg className="w-2.5 h-2.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3.5}>
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
@@ -231,7 +302,7 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
{/* Status dropdown menu */}
{showStatusMenu && (
<div className="absolute top-6 left-0 z-50 bg-white dark:bg-slate-700 rounded-lg shadow-lg border border-slate-200 dark:border-slate-600 py-1 min-w-[120px]">
<div className="absolute top-7 left-0 z-50 bg-white dark:bg-slate-700 rounded-xl shadow-xl border border-slate-200 dark:border-slate-600 py-1.5 min-w-[140px]">
{[
{ value: 'todo', label: t('statusTodo'), icon: '⬜' },
{ value: 'in_progress', label: t('statusDoing'), icon: '🔵' },
@@ -240,7 +311,7 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
<button
key={s.value}
onClick={() => handleStatusSelect(s.value)}
className={`w-full text-left px-3 py-1.5 text-sm hover:bg-slate-50 dark:hover:bg-slate-600 flex items-center gap-2 ${task.status === s.value ? 'font-semibold text-green-600 dark:text-green-400' : 'text-slate-700 dark:text-slate-200'}`}
className={`w-full text-left px-3 py-2 text-sm hover:bg-slate-50 dark:hover:bg-slate-600 flex items-center gap-2 ${task.status === s.value ? 'font-semibold text-green-600 dark:text-green-400' : 'text-slate-700 dark:text-slate-200'}`}
>
<span>{s.icon}</span>
<span>{s.label}</span>
@@ -250,24 +321,21 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
)}
</div>
{/* Content */}
<div className="flex-grow min-w-0">
{/* Title row */}
<div className="flex items-center gap-1.5 flex-wrap">
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded ${config.badge} leading-none`}>
<div className="flex-1 min-w-0 pt-0.5">
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
<span className={`text-[11px] font-bold px-2 py-0.5 rounded-md ${config.badge} leading-none tracking-wide`}>
{taskNumber}
</span>
{/* Priority badge */}
{task.priority && task.priority !== 'medium' && (
<span className={`text-[9px] font-semibold px-1.5 py-0.5 rounded ${priorityConfig[task.priority].color} leading-none`}>
<span className={`text-[11px] font-semibold px-2 py-0.5 rounded-md ${priorityConfig[task.priority].color} leading-none`}>
{t(task.priority === 'high' ? 'priorityHigh' : task.priority === 'low' ? 'priorityLow' : 'priorityMedium')}
</span>
)}
<span
onClick={handleToggle}
className={`
font-medium leading-snug
${level === 0 ? 'text-[15px]' : 'text-sm'}
min-w-0 font-semibold leading-snug
${titleSize}
${isDone ? 'text-slate-400 dark:text-slate-500 line-through decoration-slate-300' : isInProgress ? 'text-blue-700 dark:text-blue-300' : isLocked ? 'text-slate-400 dark:text-slate-500' : 'text-slate-800 dark:text-slate-100'}
${!isLocked && !hasChildren ? 'cursor-pointer' : ''}
transition-colors duration-150
@@ -275,126 +343,87 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
>
{task.title}
</span>
{/* In progress indicator */}
{isInProgress && (
<span className="text-[9px] font-semibold px-1.5 py-0.5 rounded bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 leading-none">
<span className="text-[11px] font-semibold px-2 py-0.5 rounded-md bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300 leading-none">
{t('inProgress')}
</span>
)}
</div>
</div>
{/* Description */}
<div className="hidden sm:flex flex-shrink-0 items-center gap-0.5 opacity-0 group-hover/task:opacity-100 focus-within:opacity-100 transition-opacity duration-150">
{actionButtons}
</div>
</div>
<div className="mt-2">
{task.description && (
<p className={`mt-1 text-[13px] leading-relaxed ${isDone ? 'text-slate-400 dark:text-slate-500' : isLocked ? 'text-slate-400 dark:text-slate-500' : 'text-slate-500 dark:text-slate-400'}`}>
<p className={`text-sm leading-relaxed ${isDone ? 'text-slate-400 dark:text-slate-500' : isLocked ? 'text-slate-400 dark:text-slate-500' : 'text-slate-500 dark:text-slate-400'}`}>
{task.description}
</p>
)}
{/* Meta info */}
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px]">
{task.dependencies.length > 0 && (
<div className={isDone ? 'text-slate-400 dark:text-slate-500' : 'text-slate-500 dark:text-slate-400'}>
<span className="font-medium">{t('dependsOn')}</span> {task.dependencies.join(', ')}
</div>
)}
{task.durationInHours > 0 && (
<div className={`flex items-center gap-1 ${isDone ? 'text-slate-400 dark:text-slate-500' : 'text-slate-500 dark:text-slate-400'}`}>
<ClockIcon />
<span>{task.durationInHours}h</span>
</div>
)}
{dueDateDisplay && (
<div className={`flex items-center gap-1 ${dueDateDisplay.overdue ? 'text-red-500 font-semibold' : isDone ? 'text-slate-400 dark:text-slate-500' : 'text-slate-500 dark:text-slate-400'}`}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1.5 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.25z" clipRule="evenodd" />
</svg>
<span>{dueDateDisplay.text}</span>
</div>
)}
</div>
{/* Progress bar for parent tasks */}
{hasChildren && (
<div className="mt-2.5">
<div className="flex justify-between items-center mb-1">
<span className="text-[11px] text-slate-400 dark:text-slate-500">
{t('childSummary', { done: completedChildren, total: totalChildren })}
{(task.dependencies.length > 0 || task.durationInHours > 0 || dueDateDisplay) && (
<div className={`${task.description ? 'mt-2.5' : ''} flex flex-wrap items-center gap-2 text-xs`}>
{task.dependencies.length > 0 && (
<span className={`inline-flex items-center rounded-full px-2.5 py-1 ${isDone ? 'bg-slate-100 text-slate-400 dark:bg-slate-700/50 dark:text-slate-500' : 'bg-slate-100 text-slate-500 dark:bg-slate-700/60 dark:text-slate-300'}`}>
<span className="font-medium mr-1">{t('dependsOn')}</span> {task.dependencies.join(', ')}
</span>
<span className={`text-[11px] font-bold ${childProgress === 100 ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-500 dark:text-slate-400'}`}>
)}
{task.durationInHours > 0 && (
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 ${isDone ? 'bg-slate-100 text-slate-400 dark:bg-slate-700/50 dark:text-slate-500' : 'bg-slate-100 text-slate-500 dark:bg-slate-700/60 dark:text-slate-300'}`}>
<ClockIcon />
<span>{task.durationInHours}h</span>
</span>
)}
{dueDateDisplay && (
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-1 ${dueDateDisplay.overdue ? 'bg-red-50 text-red-500 font-semibold dark:bg-red-900/20' : isDone ? 'bg-slate-100 text-slate-400 dark:bg-slate-700/50 dark:text-slate-500' : 'bg-slate-100 text-slate-500 dark:bg-slate-700/60 dark:text-slate-300'}`}>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1.5 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.25z" clipRule="evenodd" />
</svg>
<span>{dueDateDisplay.text}</span>
</span>
)}
</div>
)}
{hasChildren && (
<div className="mt-3.5">
<div className="flex justify-between items-center gap-3 mb-1.5">
<button
type="button"
onClick={() => setCollapsed(!collapsed)}
className="min-w-0 text-left text-xs leading-snug text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors"
>
{t('childSummary', { done: completedChildren, total: totalChildren })}
</button>
<span className={`shrink-0 text-xs font-bold tabular-nums ${childProgress === 100 ? 'text-emerald-600 dark:text-emerald-400' : 'text-slate-500 dark:text-slate-400'}`}>
{childProgress}%
</span>
</div>
<div className={`w-full ${config.barTrack} rounded-full h-1`}>
<div className={`w-full ${config.barTrack} rounded-full h-1.5`}>
<div
className={`h-1 rounded-full transition-all duration-500 ease-out ${childProgress === 100 ? 'bg-emerald-500' : config.barColor}`}
className={`h-1.5 rounded-full transition-all duration-500 ease-out ${childProgress === 100 ? 'bg-emerald-500' : config.barColor}`}
style={{ width: `${childProgress}%` }}
/>
</div>
</div>
)}
</div>
</div>
{/* Action buttons - always visible, compact icon row */}
<div className="flex-shrink-0 self-center flex items-center gap-0.5 opacity-0 group-hover/task:opacity-100 transition-opacity duration-150">
{/* Add sibling */}
<button
onClick={(e) => { e.stopPropagation(); onAddSiblingTask(task); }}
className="p-1.5 text-teal-500 hover:text-teal-700 dark:text-teal-400 dark:hover:text-teal-300 rounded-md hover:bg-teal-50 dark:hover:bg-teal-900/20 transition-all duration-150"
title={t('addSibling')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 2a.75.75 0 01.75.75v6.5h2.94l-1.47-1.47a.75.75 0 111.06-1.06l2.75 2.75a.75.75 0 010 1.06l-2.75 2.75a.75.75 0 11-1.06-1.06l1.47-1.47h-2.94v6.5a.75.75 0 01-1.5 0v-6.5H6.31l1.47 1.47a.75.75 0 11-1.06 1.06l-2.75-2.75a.75.75 0 010-1.06l2.75-2.75a.75.75 0 011.06 1.06L6.31 9.25h2.94v-6.5A.75.75 0 0110 2z" clipRule="evenodd" />
</svg>
</button>
{/* Add subtask */}
{level < 2 && (
<button
onClick={(e) => { e.stopPropagation(); onAddChildTask(task); }}
className="p-1.5 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all duration-150"
title={t('addChild')}
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M2 4.75A.75.75 0 012.75 4h3a.75.75 0 010 1.5H4.5v7.75a2.25 2.25 0 002.25 2.25h4.19l-1.47-1.47a.75.75 0 111.06-1.06l2.75 2.75a.75.75 0 010 1.06l-2.75 2.75a.75.75 0 11-1.06-1.06l1.47-1.47H6.75A3.75 3.75 0 013 12.25V5.5H2.75A.75.75 0 012 4.75z" clipRule="evenodd" />
</svg>
</button>
)}
{/* Edit */}
<button
onClick={(e) => { e.stopPropagation(); onEditTask(task); }}
className="p-1.5 text-emerald-500 hover:text-emerald-700 dark:text-emerald-400 dark:hover:text-emerald-300 rounded-md hover:bg-emerald-50 dark:hover:bg-emerald-900/20 transition-all duration-150"
title={t('editTask')}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3.5 h-3.5">
<path d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
</svg>
</button>
{/* Delete */}
<button
onClick={(e) => { e.stopPropagation(); setShowDeleteConfirm(true); }}
className="p-1.5 text-red-400 hover:text-red-600 dark:text-red-400 dark:hover:text-red-300 rounded-md hover:bg-red-50 dark:hover:bg-red-900/20 transition-all duration-150"
title={t('deleteTask')}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3.5 h-3.5">
<path fillRule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022 1.005 11.27A2.75 2.75 0 007.77 19h4.46a2.75 2.75 0 002.751-2.529l1.005-11.27.149.022a.75.75 0 10.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clipRule="evenodd" />
</svg>
</button>
</div>
<div className="mt-1.5 flex sm:hidden items-center justify-end gap-0.5 -mr-1">
{actionButtons}
</div>
</div>
{/* Children with tree lines */}
{hasChildren && !collapsed && (
<div className="relative" style={{ marginLeft: indentPx }}>
<div className="absolute top-0 bottom-0 w-px bg-slate-200 dark:bg-slate-600" style={{ left: 14 }} />
<div className="pt-1 space-y-0.5">
{task.children.map((child, index) => {
const isLast = index === task.children!.length - 1;
return (
<div key={child.id} className="relative">
<div className="absolute top-5 h-px bg-slate-200 dark:bg-slate-600" style={{ left: 14, width: 14 }} />
{!isLast && <div className="absolute top-0 bottom-0 w-px bg-slate-200 dark:bg-slate-600" style={{ left: 14 }} />}
{isLast && <div className="absolute top-0 h-5 w-px bg-slate-200 dark:bg-slate-600" style={{ left: 14 }} />}
<TaskItem
<div className={`mt-2.5 sm:ml-8 rounded-2xl border p-2 sm:p-4 space-y-2.5 sm:space-y-3 ${config.nest}`}>
{task.children.map((child, index) => {
const isLast = index === task.children!.length - 1;
return (
<div key={child.id} className="relative sm:pl-4">
<span className={`hidden sm:block absolute left-0 top-5 bottom-5 w-0.5 rounded-full ${config.rail}`} aria-hidden="true" />
<TaskItem
task={child}
taskNumber={`${taskNumber}.${index + 1}`}
onToggleTask={onToggleTask}
@@ -414,10 +443,9 @@ const TaskItem: React.FC<TaskItemProps> = ({ task, taskNumber, onToggleTask, onE
onChildTaskCompleted={onChildTaskCompleted}
onParentTaskCompleted={onParentTaskCompleted}
/>
</div>
);
})}
</div>
</div>
);
})}
</div>
)}
+1 -1
View File
@@ -48,7 +48,7 @@ const TaskList: React.FC<TaskListProps> = ({ tasks, onToggleTask, onEditTask, on
};
return (
<div className="space-y-2">
<div className="space-y-4 sm:space-y-5">
{tasks.map((task, index) => (
<TaskItem
key={task.id}
+1 -1
View File
@@ -46,7 +46,7 @@ const zh = {
namePlaceholder: '你的名字',
hasAccount: '已经有账号?',
nextGoalTitle: '下一个大目标是什么?',
nextGoalTitle: '下一个伟大目标是什么?',
nextGoalSub: '描述你想完成的事,我们来拆步骤。',
goalPlaceholder: '例如:做一个分享烹饪的个人博客',
collapseExtra: '收起',
+1 -1
View File
@@ -34,7 +34,7 @@
}
</script>
</head>
<body class="bg-slate-50 text-slate-800">
<body class="bg-slate-50 text-slate-800 antialiased">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>