first commit

This commit is contained in:
Aculix Technologies 2025-10-16 14:16:01 +05:30
commit 876ba3c835
12 changed files with 1149 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.DS_Store
/node_modules
/package-lock.json

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies with proper optional dependency handling for Alpine/musl
RUN npm install --include=optional
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration (if needed)
# COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Aculix Technologies LLP
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

113
README.md Normal file
View File

@ -0,0 +1,113 @@
# Negotium
A beautiful, minimal to-do list application featuring smooth animations, intelligent date management, and a modern design that helps you stay organized and productive.
## 💭 Why Negotium?
While powerful tools like Trello and Vikunja excel at managing complex projects and long-term planning, sometimes you just need a simple, focused space for your daily tasks. That's why I built Negotium, a straightforward to-do list for today and tomorrow. Nothing more, nothing less.
Built with Svelte for speed and simplicity. No overwhelming features, no endless project boards, no complexity. Just a clean interface for your daily workflow.
## ✨ Features
### Core Functionality
- ✅ **Add, complete, and delete tasks** with smooth animations
- 📅 **Today & Tomorrow lists** - Plan ahead with separate task lists
- 🔄 **Automatic task migration** - Tomorrow's tasks automatically move to Today when a new day begins
- 🎯 **Drag and drop reordering** - Organize tasks by dragging them into position
- 💾 **Persistent storage** - All tasks saved locally in your browser
- 📊 **Task statistics** - See remaining and completed tasks at a glance
## 🚀 Getting Started
### Quick Start with Docker (Recommended)
Pull and run the pre-built Docker image:
```bash
# Pull the image
docker pull ghcr.io/aculix/negotium:main
# Run the container
docker run -d -p 3000:80 --name negotium ghcr.io/aculix/negotium:main
```
Then open `http://localhost:3000` in your browser.
To stop the container:
```bash
docker stop negotium
docker rm negotium
```
### Manual Installation
If you prefer to run the application locally without Docker:
1. Clone the repository:
```bash
git clone <repository-url>
cd negotium
```
2. Install dependencies:
```bash
npm install
```
3. Start the development server:
```bash
npm run dev
```
4. Open `http://localhost:3000` in your browser
#### Build for Production
```bash
npm run build
```
The optimized files will be in the `dist` directory.
## 📖 How to Use
### Managing Tasks
- **Add a task**: Type in the input field and press Enter
- **Complete a task**: Click the checkbox next to the task
- **Delete a task**: Hover over a task and click the delete icon
- **Reorder tasks**: Click and drag any task to a new position
- **Clear input**: Press Escape to clear the input field
### Date Management
- **Switch between Today and Tomorrow**: Click the date button in the header
- **Plan ahead**: Add tasks to Tomorrow's list before you need them
- **Automatic migration**: When a new day begins, Tomorrow's tasks automatically become Today's tasks
- **Separate lists**: Today and Tomorrow maintain independent task lists
### Theme Toggle
- Click the sun/moon icon in the header to switch themes
- Your preference is saved automatically and restored on reload
- Respects system dark mode preference on first visit
### Keyboard Shortcuts
- **Enter**: Add task (when input is focused)
- **Escape**: Clear input field
- **Space/Enter**: Toggle task completion (when task is focused)
- **Delete/Backspace**: Delete task (when task is focused)
## 💾 Data Storage
All data is stored locally in your browser using localStorage:
- **Tasks**: Separate storage keys for each date (`negotium-tasks-<date>`)
- **Theme**: Your theme preference (`negotium-theme`)
- **No server required**: Everything runs entirely client-side
- **Privacy first**: Your data never leaves your device
## 📄 License
MIT License - Free for personal and commercial use.
## 🤝 Contributing
Contributions are welcome! Feel free to submit issues and pull requests.

8
assets/logo.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.37 8.87988H17.62" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.38 8.87988L7.13 9.62988L9.38 7.37988" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.37 15.8799H17.62" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.38 15.8799L7.13 16.6299L9.38 14.3799" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 908 B

File diff suppressed because one or more lines are too long

31
index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Negotium - Your Productivity Companion</title>
<meta name="description" content="A clean, minimal to-do list application with smooth animations and dark/light mode support">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'><path d='M12.37 8.87988H17.62' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M6.38 8.87988L7.13 9.62988L9.38 7.37988' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M12.37 15.8799H17.62' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M6.38 15.8799L7.13 16.6299L9.38 14.3799' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/><path d='M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z' stroke='%23607afb' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>">
<style>
/* Prevent flash of unstyled content */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #F8FAFB;
color: #1A1A1A;
transition: background-color 300ms ease, color 300ms ease;
}
.dark {
background-color: #121212;
color: #E0E0E0;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "negotium-todo",
"version": "1.0.0",
"description": "A clean, minimal to-do list application with smooth animations and dark/light mode support",
"type": "module",
"main": "index.html",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^4.2.0",
"vite": "^5.0.0"
},
"keywords": [
"todo",
"svelte",
"productivity",
"minimal"
],
"author": "Negotium",
"license": "MIT",
"dependencies": {
"lottie-web": "^5.13.0"
}
}

371
src/App.svelte Normal file
View File

@ -0,0 +1,371 @@
<script>
import { onMount } from 'svelte';
import { fly, fade } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import lottie from 'lottie-web';
import './style.css';
let tasks = [];
let newTask = '';
let darkMode = false;
let inputElement;
let isLoading = true;
let isInitialized = false;
let currentDate = new Date().toDateString();
let selectedDate = new Date().toDateString();
let draggedItem = null;
let draggedOverIndex = null;
function addTask() {
if (newTask.trim()) {
tasks = [...tasks, {
id: Date.now(),
text: newTask.trim(),
completed: false,
createdAt: Date.now()
}];
newTask = '';
}
}
function toggleTask(id) {
tasks = tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
);
}
function deleteTask(id) {
tasks = tasks.filter(task => task.id !== id);
}
function toggleTheme() {
darkMode = !darkMode;
}
function handleKeydown(event) {
if (event.key === 'Enter') addTask();
else if (event.key === 'Escape') newTask = '';
}
function handleTaskKeydown(event, taskId) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
toggleTask(taskId);
} else if (event.key === 'Delete' || event.key === 'Backspace') {
deleteTask(taskId);
}
}
function handleDragStart(event, index) {
draggedItem = index;
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/html', event.target);
}
function handleDragOver(event, index) {
event.preventDefault();
draggedOverIndex = index;
}
function handleDragEnd(event) {
event.preventDefault();
if (draggedItem !== null && draggedOverIndex !== null && draggedItem !== draggedOverIndex) {
const newTasks = [...tasks];
const [movedTask] = newTasks.splice(draggedItem, 1);
newTasks.splice(draggedOverIndex, 0, movedTask);
tasks = newTasks;
}
draggedItem = null;
draggedOverIndex = null;
}
function handleDragLeave() {
draggedOverIndex = null;
}
function getCurrentDate() {
return new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function getDateKey(dateString) {
return `negotium-tasks-${dateString}`;
}
function loadTasksForDate(dateString) {
const savedTasks = localStorage.getItem(getDateKey(dateString));
if (savedTasks) {
tasks = JSON.parse(savedTasks);
} else {
tasks = [];
}
}
function saveTasksForDate(dateString) {
localStorage.setItem(getDateKey(dateString), JSON.stringify(tasks));
}
function checkAndMigrateTasks() {
const today = new Date().toDateString();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayString = yesterday.toDateString();
const yesterdayTasks = localStorage.getItem(getDateKey(yesterdayString));
if (yesterdayTasks && currentDate !== today) {
const tasks = JSON.parse(yesterdayTasks);
const todayTasks = localStorage.getItem(getDateKey(today));
if (todayTasks) {
const existingTodayTasks = JSON.parse(todayTasks);
localStorage.setItem(getDateKey(today), JSON.stringify([...existingTodayTasks, ...tasks]));
} else {
localStorage.setItem(getDateKey(today), yesterdayTasks);
}
localStorage.removeItem(getDateKey(yesterdayString));
currentDate = today;
}
}
function switchDate() {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
selectedDate = selectedDate === today.toDateString()
? tomorrow.toDateString()
: today.toDateString();
loadTasksForDate(selectedDate);
}
$: buttonText = (() => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (selectedDate === today.toDateString()) return 'Today';
if (selectedDate === tomorrow.toDateString()) return 'Tomorrow';
return selectedDate.split(' ').slice(0, 3).join(' ');
})();
$: remainingTasks = tasks.filter(task => !task.completed).length;
$: completedTasks = tasks.filter(task => task.completed).length;
$: if (tasks && isInitialized) {
saveTasksForDate(selectedDate);
}
$: if (darkMode !== undefined && isInitialized) {
localStorage.setItem('negotium-theme', darkMode ? 'dark' : 'light');
}
function initLottie(node) {
let instance = null;
async function loadAnimation() {
try {
const response = await fetch('/lottie_empty_state.json');
const animationData = await response.json();
instance = lottie.loadAnimation({
container: node,
renderer: 'svg',
loop: true,
autoplay: true,
animationData: animationData
});
} catch (error) {
console.error('Failed to load Lottie animation:', error);
}
}
loadAnimation();
return {
destroy() {
if (instance) {
instance.destroy();
}
}
};
}
onMount(() => {
checkAndMigrateTasks();
loadTasksForDate(selectedDate);
const savedTheme = localStorage.getItem('negotium-theme');
darkMode = savedTheme ? savedTheme === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
setTimeout(() => {
isLoading = false;
isInitialized = true;
}, 500);
});
</script>
<svelte:window on:keydown={handleKeydown} />
<div class="app" class:dark={darkMode}>
{#if isLoading}
<div class="loading-overlay" transition:fade={{ duration: 300 }}>
<div class="loading">
<svg class="loading-logo" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.37 8.87988H17.62" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.38 8.87988L7.13 9.62988L9.38 7.37988" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.37 15.8799H17.62" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.38 15.8799L7.13 16.6299L9.38 14.3799" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<div class="loading-text">Loading Negotium...</div>
</div>
</div>
{:else}
<header class="header">
<div class="header-content">
<div class="logo-section">
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.37 8.87988H17.62" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.38 8.87988L7.13 9.62988L9.38 7.37988" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.37 15.8799H17.62" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.38 15.8799L7.13 16.6299L9.38 14.3799" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h1 class="app-title">Negotium</h1>
</div>
<div class="header-actions">
<button class="today-btn" aria-label="Switch between Today and Tomorrow" on:click={switchDate}>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/>
<line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/>
<line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/>
</svg>
{buttonText}
</button>
<button
class="theme-toggle"
on:click={toggleTheme}
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
<svg class="theme-icon" class:rotated={darkMode} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{#if darkMode}
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="currentColor"/>
{:else}
<circle cx="12" cy="12" r="5" fill="currentColor"/>
<line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2"/>
<line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2"/>
<line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2"/>
<line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2"/>
{/if}
</svg>
</button>
</div>
</div>
</header>
<main class="main">
<div class="container">
<div class="content-header">
<h2 class="section-title">To-dos</h2>
<div class="date-display">{getCurrentDate()}</div>
</div>
<div class="task-input-container">
<input
bind:this={inputElement}
bind:value={newTask}
type="text"
placeholder="+ Add a task"
class="task-input"
on:keydown={handleKeydown}
/>
</div>
<div class="task-stats" class:visible={tasks.length > 0}>
<span class="task-count">
{remainingTasks} {remainingTasks === 1 ? 'task' : 'tasks'} remaining
</span>
{#if completedTasks > 0}
<button class="clear-completed" on:click={() => tasks = tasks.filter(task => !task.completed)}>
Clear completed
</button>
{/if}
</div>
<div class="task-list">
{#key selectedDate}
{#if tasks.length === 0}
<div class="empty-state" transition:fade={{ duration: 200 }}>
<div class="lottie-animation" use:initLottie></div>
<p>No tasks yet. Add one above to get started!</p>
</div>
{:else}
{#each tasks as task, index (task.id)}
<div
class="task-item"
class:completed={task.completed}
class:dragging={draggedItem === index}
class:drag-over={draggedOverIndex === index}
draggable="true"
in:fly={{ y: -10, duration: 300, delay: index * 30, easing: cubicOut }}
out:fly={{ x: 30, opacity: 0, duration: 250, delay: index * 20, easing: cubicOut }}
on:dragstart={(e) => handleDragStart(e, index)}
on:dragover={(e) => handleDragOver(e, index)}
on:dragend={handleDragEnd}
on:dragleave={handleDragLeave}
on:keydown={(e) => handleTaskKeydown(e, task.id)}
tabindex="0"
role="button"
aria-label={task.completed ? `Completed: ${task.text}` : `Incomplete: ${task.text}`}
>
<button
class="checkbox"
class:checked={task.completed}
on:click={() => toggleTask(task.id)}
aria-label={task.completed ? 'Mark as incomplete' : 'Mark as complete'}
>
{#if task.completed}
<svg class="checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 12L11 14L15 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{/if}
</button>
<span class="task-text">{task.text}</span>
<button
class="delete-btn"
on:click={() => deleteTask(task.id)}
aria-label="Delete task"
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<polyline points="3,6 5,6 21,6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19,6V20A2,2 0 0,1 17,22H7A2,2 0 0,1 5,20V6M8,6V4A2,2 0 0,1 10,2H14A2,2 0 0,1 16,4V6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="10" y1="11" x2="10" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="14" y1="11" x2="14" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
{/each}
{/if}
{/key}
</div>
</div>
</main>
{/if}
</div>

7
src/main.js Normal file
View File

@ -0,0 +1,7 @@
import App from './App.svelte';
const app = new App({
target: document.getElementById('app'),
});
export default app;

521
src/style.css Normal file
View File

@ -0,0 +1,521 @@
:root {
--bg-primary: #F8FAFB;
--bg-surface: #FFFFFF;
--text-primary: #1A1A1A;
--text-secondary: #666666;
--accent: #607afb;
--border: #E0E0E0;
--hover: #F5F5F5;
--completed-bg: #EEF1FF;
--completed-text: #4C5FD5;
--transition-speed: 300ms;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.dark {
--bg-primary: #121212;
--bg-surface: #1E1E1E;
--text-primary: #E0E0E0;
--text-secondary: #999999;
--accent: #7B93FF;
--border: #333333;
--hover: #2A2A2A;
--completed-bg: rgba(96, 122, 251, 0.15);
--completed-text: #8FA5FF;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-family);
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease;
}
.app {
min-height: 100vh;
background-color: var(--bg-primary);
transition: background-color var(--transition-speed) ease;
}
.header {
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border);
padding: 24px 48px;
position: sticky;
top: 0;
z-index: 100;
transition: all var(--transition-speed) ease;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo-section {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 32px;
height: 32px;
color: var(--accent);
}
.app-title {
font-size: 28px;
font-weight: bold;
color: var(--text-primary);
transition: color var(--transition-speed) ease;
}
.header-actions {
display: flex;
align-items: center;
gap: 16px;
}
.today-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: transparent;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
transition: all 200ms ease;
height: 40px;
}
.today-btn:hover {
background-color: var(--hover);
border-color: var(--accent);
color: var(--accent);
}
.today-btn svg {
width: 16px;
height: 16px;
}
.theme-toggle {
padding: 8px 16px;
border-radius: 8px;
background: transparent;
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms ease;
font-size: 14px;
height: 40px;
}
.theme-toggle:hover {
background-color: var(--hover);
border-color: var(--accent);
color: var(--accent);
transform: scale(1.1);
}
.theme-toggle:active {
transform: scale(0.95);
}
.theme-icon {
width: 20px;
height: 20px;
transition: transform 500ms ease;
}
.theme-icon.rotated {
transform: rotate(360deg);
}
.main {
padding: 40px 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 32px;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.section-title {
font-size: 32px;
font-weight: bold;
color: var(--text-primary);
transition: color var(--transition-speed) ease;
}
.date-display {
font-size: 16px;
font-weight: 500;
color: var(--accent);
}
.task-input-container {
margin-bottom: 24px;
}
.task-input {
width: 100%;
padding: 16px 20px;
font-size: 16px;
border: 1px solid var(--border);
border-radius: 12px;
background-color: var(--bg-surface);
color: var(--text-primary);
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
font-family: var(--font-family);
}
.task-input:focus {
outline: none;
border: 1px solid var(--border);
background-color: var(--bg-primary);
box-shadow: 0 8px 24px rgba(96, 122, 251, 0.12), 0 4px 8px rgba(0, 0, 0, 0.06);
transform: scale(1.01);
}
.task-input::placeholder {
color: var(--text-secondary);
}
.task-stats {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
opacity: 0;
transition: opacity 300ms ease;
}
.task-stats.visible {
opacity: 1;
}
.task-count {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.clear-completed {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 14px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all 200ms ease;
}
.clear-completed:hover {
color: var(--accent);
background-color: var(--hover);
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
min-height: 400px;
position: relative;
}
.task-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
background-color: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
cursor: grab;
transition: all 300ms ease;
position: relative;
}
.task-item:active {
cursor: grabbing;
}
.task-item.dragging {
opacity: 0.4;
cursor: grabbing;
}
.task-item.drag-over::before {
content: '';
position: absolute;
top: -6px;
left: 0;
right: 0;
height: 3px;
background-color: var(--accent);
border-radius: 2px;
box-shadow: 0 0 8px rgba(96, 122, 251, 0.4);
z-index: 10;
}
.task-item:hover {
background-color: var(--hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.task-item.completed {
background-color: var(--completed-bg);
opacity: 0.8;
}
.checkbox {
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-radius: 6px;
background: transparent;
cursor: pointer;
display: block;
transition: all 300ms ease;
flex-shrink: 0;
position: relative;
}
.checkbox:hover {
border-color: var(--accent);
transform: scale(1.1);
}
.checkbox.checked {
background-color: var(--accent);
border-color: var(--accent);
transform: scale(1.1);
}
.checkbox.checked:hover {
transform: scale(1.2);
}
.checkmark {
width: 16px;
height: 16px;
color: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.task-text {
flex: 1;
font-size: 16px;
color: var(--text-primary);
transition: all 300ms ease;
word-break: break-word;
}
.task-item.completed .task-text {
text-decoration: line-through;
color: var(--completed-text);
}
.delete-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 200ms ease;
flex-shrink: 0;
}
.task-item:hover .delete-btn {
opacity: 1;
}
.delete-btn:hover {
background-color: rgba(244, 67, 54, 0.1);
color: #f44336;
transform: scale(1.1);
}
.delete-btn svg {
width: 16px;
height: 16px;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-logo {
width: 60px;
height: 60px;
color: var(--accent);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.95);
}
}
.loading-text {
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.lottie-animation {
width: 200px;
height: 200px;
margin: 0 auto 24px;
}
.empty-state p {
font-size: 16px;
margin: 0;
}
@media (max-width: 768px) {
.header {
padding: 16px 24px;
}
.header-content {
flex-direction: column;
gap: 16px;
align-items: flex-start;
}
.header-actions {
width: 100%;
justify-content: space-between;
}
.container {
padding: 0 24px;
}
.content-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.section-title {
font-size: 28px;
}
.task-item {
padding: 20px;
min-height: 48px;
}
.task-input {
padding: 20px;
font-size: 18px;
}
}
@media (max-width: 480px) {
.header {
padding: 12px 16px;
}
.container {
padding: 0 16px;
}
.task-item {
padding: 16px;
}
.task-input {
padding: 16px;
}
}
.checkbox:focus-visible,
.delete-btn:focus-visible,
.theme-toggle:focus-visible,
.today-btn:focus-visible,
.clear-completed:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
html {
scroll-behavior: smooth;
}

14
vite.config.mjs Normal file
View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
plugins: [svelte()],
publicDir: 'assets',
server: {
port: 3000,
open: true
},
optimizeDeps: {
exclude: ['@sveltejs/vite-plugin-svelte']
}
})