From ba385e116bd9aac4b3e2b270e16f9befa021e26d Mon Sep 17 00:00:00 2001 From: Dang Tran <91554483+BrooklynD23@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:50:52 -0800 Subject: [PATCH 1/9] feat: Make insertion button always visible but faded for accessibility --- package-lock.json | 13 +++++++++++++ src/components/InsertionPoint.tsx | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f9e02e0..a33e651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -155,6 +155,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -475,6 +476,7 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -515,6 +517,7 @@ "version": "11.14.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1141,6 +1144,7 @@ "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.6", @@ -1744,6 +1748,7 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1812,6 +1817,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -1993,6 +1999,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2138,6 +2145,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2401,6 +2409,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3428,6 +3437,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3439,6 +3449,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3801,6 +3812,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3853,6 +3865,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/components/InsertionPoint.tsx b/src/components/InsertionPoint.tsx index c49980e..50c3316 100644 --- a/src/components/InsertionPoint.tsx +++ b/src/components/InsertionPoint.tsx @@ -26,7 +26,7 @@ export const InsertionPoint = ({ display: 'flex', alignItems: 'center', justifyContent: 'center', - opacity: isHovered || isActive ? 1 : 0, + opacity: isHovered || isActive ? 1 : 0.35, transition: 'opacity 150ms ease', cursor: 'pointer', position: 'relative', From cef6858ea56e829fc501234e1138f66b83881993 Mon Sep 17 00:00:00 2001 From: Dang Tran <91554483+BrooklynD23@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:52:35 -0800 Subject: [PATCH 2/9] feat: Add end zone drop target to allow moving tasks below locked tasks --- src/components/EndZoneDropTarget.tsx | 97 ++++++++++++++++++++++++++++ src/components/Timeline.tsx | 20 ++++++ src/store/taskStore.ts | 14 ++++ src/types/index.ts | 1 + src/utils/dragLogic.ts | 37 +++++++++++ 5 files changed, 169 insertions(+) create mode 100644 src/components/EndZoneDropTarget.tsx diff --git a/src/components/EndZoneDropTarget.tsx b/src/components/EndZoneDropTarget.tsx new file mode 100644 index 0000000..d98d1c7 --- /dev/null +++ b/src/components/EndZoneDropTarget.tsx @@ -0,0 +1,97 @@ +import { Box, Typography } from '@mui/material' +import { useEffect, useRef, useState } from 'react' +import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter' + +interface EndZoneDropTargetProps { + onDrop: (draggedTaskId: string) => void + hasLockedTaskAtEnd: boolean +} + +/** + * EndZoneDropTarget - a drop zone at the bottom of the task list + * Allows users to move tasks to the end, even past locked tasks + */ +export const EndZoneDropTarget = ({ + onDrop, + hasLockedTaskAtEnd, +}: EndZoneDropTargetProps) => { + const dropRef = useRef(null) + const [isDropTarget, setIsDropTarget] = useState(false) + const [isDragging, setIsDragging] = useState(false) + + useEffect(() => { + const element = dropRef.current + if (!element) return + + const cleanup = dropTargetForElements({ + element, + getData: () => ({ type: 'end-zone' }), + onDragEnter: () => { + setIsDropTarget(true) + setIsDragging(true) + }, + onDragLeave: () => { + setIsDropTarget(false) + }, + onDrop: ({ source }) => { + setIsDropTarget(false) + setIsDragging(false) + const draggedTaskId = source.data.taskId as string + if (draggedTaskId) { + onDrop(draggedTaskId) + } + }, + }) + + // Listen for global drag events to show the zone + const handleDragStart = () => setIsDragging(true) + const handleDragEnd = () => { + setIsDragging(false) + setIsDropTarget(false) + } + + document.addEventListener('dragstart', handleDragStart) + document.addEventListener('dragend', handleDragEnd) + + return () => { + cleanup() + document.removeEventListener('dragstart', handleDragStart) + document.removeEventListener('dragend', handleDragEnd) + } + }, [onDrop]) + + // Only show when there's a locked task at the end and we're dragging + if (!hasLockedTaskAtEnd && !isDragging) return null + + return ( + + + {isDropTarget ? 'Drop here to move to end' : 'Drop zone'} + + + ) +} + diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index ba28a4b..c17da2b 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -5,6 +5,7 @@ import { TaskBlock } from './TaskBlock' import { InsertionPoint } from './InsertionPoint' import { InlineTaskForm } from './InlineTaskForm' import { TimeLabel } from './TimeLabel' +import { EndZoneDropTarget } from './EndZoneDropTarget' import { generateTimeLabels, getVisibleTimeRange } from '../utils/timeCalculations' import { getDragAction } from '../utils/dragLogic' @@ -22,6 +23,7 @@ export const Timeline = () => { const toggleLock = useTaskStore((state) => state.toggleLock) const swapTasks = useTaskStore((state) => state.swapTasks) const pushTask = useTaskStore((state) => state.pushTask) + const moveToEnd = useTaskStore((state) => state.moveToEnd) const dragConfig = useTaskStore((state) => state.dragConfig) const [activeInsertionPoint, setActiveInsertionPoint] = useState(null) @@ -32,6 +34,10 @@ export const Timeline = () => { const { start, end } = getVisibleTimeRange(tasks) const timeLabels = generateTimeLabels(start, end) + // Check if the last task is locked (for showing end zone drop target) + const lastTask = tasks[tasks.length - 1] + const hasLockedTaskAtEnd = lastTask?.isLocked ?? false + const handleInsertTask = (afterTaskId: string | null, title: string, durationMinutes: number) => { insertTask(afterTaskId, { title, @@ -55,6 +61,12 @@ export const Timeline = () => { setDraggingTaskId(null) } + const handleMoveToEnd = (taskId: string) => { + console.log('Move to end:', taskId) + moveToEnd(taskId) + setDraggingTaskId(null) + } + const handleDrop = (draggedTaskId: string, targetTaskId: string, dropY: number) => { const draggedTask = tasks.find((t) => t.id === draggedTaskId) const targetTask = tasks.find((t) => t.id === targetTaskId) @@ -188,6 +200,14 @@ export const Timeline = () => { )) )} + + {/* End zone drop target - shown when dragging, especially useful when last task is locked */} + {tasks.length > 0 && ( + + )} {/* Info text at bottom */} diff --git a/src/store/taskStore.ts b/src/store/taskStore.ts index 50a5108..683a784 100644 --- a/src/store/taskStore.ts +++ b/src/store/taskStore.ts @@ -10,6 +10,7 @@ import { executePush, detectOverlaps, reorderTasks, + moveTaskToEnd, } from '../utils/dragLogic' import { taskColors } from '../theme/theme' @@ -138,6 +139,19 @@ export const useTaskStore = create((set, get) => ({ // If result is null, operation failed (e.g., locked conflict) - do nothing }, + /** + * Move task to the end of the list (useful for moving past locked tasks) + */ + moveToEnd: (taskId) => { + const result = moveTaskToEnd(get().tasks, taskId) + + if (result) { + const tasksWithOverlaps = detectOverlaps(result) + set({ tasks: tasksWithOverlaps }) + saveTasksToStorage(tasksWithOverlaps) + } + }, + // State Management /** diff --git a/src/types/index.ts b/src/types/index.ts index 258a5c1..7b3b3fc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -53,6 +53,7 @@ export interface TaskStore extends AppState { insertTask: (afterTaskId: string | null, task: Omit) => void swapTasks: (taskId1: string, taskId2: string) => void pushTask: (draggedTaskId: string, targetTaskId: string) => void + moveToEnd: (taskId: string) => void // State management toggleLock: (taskId: string) => void diff --git a/src/utils/dragLogic.ts b/src/utils/dragLogic.ts index 51b988e..7f800f7 100644 --- a/src/utils/dragLogic.ts +++ b/src/utils/dragLogic.ts @@ -177,3 +177,40 @@ export const detectOverlaps = (tasks: Task[]): Task[] => { export const reorderTasks = (tasks: Task[]): Task[] => { return tasks.map((task, index) => ({ ...task, order: index })) } + +/** + * Move a task to the end of the task list + * This allows moving tasks below locked tasks when there's no drop target after them + */ +export const moveTaskToEnd = ( + tasks: Task[], + draggedTaskId: string +): Task[] | null => { + const draggedTask = tasks.find((t) => t.id === draggedTaskId) + + if (!draggedTask) return null + if (draggedTask.isLocked) return null // Can't move locked tasks + + // Get the current max order + const maxOrder = Math.max(...tasks.map((t) => t.order)) + + // If task is already at the end, no change needed + if (draggedTask.order === maxOrder) return null + + // Move dragged task to end, shift other tasks up + const draggedOrder = draggedTask.order + + const reorderedTasks = tasks.map((task) => { + if (task.id === draggedTaskId) { + // Dragged task goes to the end + return { ...task, order: maxOrder } + } else if (task.order > draggedOrder) { + // Tasks after the dragged task shift up by 1 + return { ...task, order: task.order - 1 } + } + return task + }) + + // Recalculate times based on new order + return recalculateTaskTimes(reorderedTasks) +} \ No newline at end of file From a48ade578c4d17ec52fe293818ef52d1435accd0 Mon Sep 17 00:00:00 2001 From: Dang Tran <91554483+BrooklynD23@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:57:38 -0800 Subject: [PATCH 3/9] fix: Show [+] button only at top, remove insertion points between tasks --- src/components/Timeline.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index c17da2b..70cfa49 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -137,7 +137,7 @@ export const Timeline = () => { /> )} - {/* Tasks with time labels and insertion points */} + {/* Tasks with time labels */} {tasks.length === 0 ? ( { /> - - {/* Insertion point after this task */} - {activeInsertionPoint === task.id ? ( - handleInsertTask(task.id, title, duration)} - onCancel={handleCancelInsert} - /> - ) : ( - setActiveInsertionPoint(task.id)} - isActive={activeInsertionPoint === task.id} - /> - )} )) )} @@ -214,7 +201,7 @@ export const Timeline = () => { {tasks.length > 0 && ( - Phase 3 Complete: UI Components ready! Drag-and-drop coming in Phase 4. + Tip: Create tasks with [+] at top, then drag to reorder )} From c0fb3f282968cc55f55f886b11f485eec20e311c Mon Sep 17 00:00:00 2001 From: Dang Tran <91554483+BrooklynD23@users.noreply.github.com> Date: Mon, 5 Jan 2026 23:08:15 -0800 Subject: [PATCH 4/9] feat: Add 15-min rounding, smart slot finding respecting locked tasks, highlight new tasks - Duration now rounds up to nearest 15 minutes with helper text - New tasks find available slots without moving locked tasks - Locked tasks keep their fixed time positions - New tasks show green highlight bar on right side for 3 seconds - recalculateTaskTimes now respects locked tasks' positions --- src/components/InlineTaskForm.tsx | 43 +++++--- src/components/TaskBlock.tsx | 50 ++++++++- src/store/taskStore.ts | 43 +++++--- src/types/index.ts | 1 + src/utils/storage.ts | 6 +- src/utils/timeCalculations.ts | 170 +++++++++++++++++++++++++++++- 6 files changed, 279 insertions(+), 34 deletions(-) diff --git a/src/components/InlineTaskForm.tsx b/src/components/InlineTaskForm.tsx index 885b726..91e1c3a 100644 --- a/src/components/InlineTaskForm.tsx +++ b/src/components/InlineTaskForm.tsx @@ -1,5 +1,6 @@ -import { Box, TextField, Button } from '@mui/material' +import { Box, TextField, Button, Typography } from '@mui/material' import { useState, useEffect, useRef } from 'react' +import { roundDurationUp } from '../utils/timeCalculations' interface InlineTaskFormProps { onSubmit: (title: string, durationMinutes: number) => void @@ -13,6 +14,7 @@ interface InlineTaskFormProps { * - Autofocus on title input * - Enter to submit, Escape to cancel * - Default duration: 30 minutes + * - Duration is rounded up to nearest 15 minutes */ export const InlineTaskForm = ({ onSubmit, @@ -22,6 +24,9 @@ export const InlineTaskForm = ({ const [duration, setDuration] = useState(30) const titleInputRef = useRef(null) + // Calculate rounded duration for display + const roundedDuration = roundDurationUp(duration) + // Autofocus on mount useEffect(() => { titleInputRef.current?.focus() @@ -38,7 +43,9 @@ export const InlineTaskForm = ({ return // Minimum 1 minute } - onSubmit(title.trim(), duration) + // Round up duration to nearest 15 minutes before submitting + const finalDuration = roundDurationUp(duration) + onSubmit(title.trim(), finalDuration) setTitle('') setDuration(30) } @@ -73,19 +80,25 @@ export const InlineTaskForm = ({ sx={{ mb: 1.5 }} /> - setDuration(parseInt(e.target.value) || 0)} - inputProps={{ - min: 1, - step: 1, - }} - sx={{ mb: 1.5 }} - /> + + setDuration(parseInt(e.target.value) || 0)} + inputProps={{ + min: 1, + step: 15, + }} + helperText={ + duration !== roundedDuration + ? `Rounds up to ${roundedDuration} min (15-min intervals)` + : 'Duration in 15-minute intervals' + } + /> + + )} + + + {/* Add Task Button - Positioned relative to max content width */} + setActiveInsertionPoint('start')} + sx={{ + position: 'absolute', + top: { xs: 16, sm: 28 }, + right: { + xs: 16, + sm: `max(16px, calc(50% - ${CONTENT_MAX_WIDTH / 2}px - 76px))` + }, + width: 56, + height: 56, + backgroundColor: '#FFFFFF', + boxShadow: '0 8px 24px rgba(0,0,0,0.08)', + border: '1px solid rgba(0,0,0,0.04)', + '&:hover': { + backgroundColor: '#F8F8F8', + boxShadow: '0 12px 32px rgba(0,0,0,0.12)', + }, + }} + > + + + + {/* Date Navigation - Centered */} + + handleDateChange('prev')} + > + + + Yesterday + + + + + {dateLabel} + + + handleDateChange('next')} + > + + Tomorrow + + + + + + {/* Main Content Container - Dynamically Centered */} + + {/* Inline Form at Start */} + {activeInsertionPoint === 'start' && ( + + handleInsertTask(null, title, duration)} + onCancel={() => setActiveInsertionPoint(null)} + /> + + )} + + {/* Empty State - Centered within content container */} + {tasks.length === 0 ? ( + + + No tasks scheduled for this day. Click the + button to add your first task. + + + ) : ( + /* Tasks List */ + tasks.map((task) => ( + + {/* Insertion Point Before Task */} + + {activeInsertionPoint === task.id ? ( + handleInsertTask(task.id, title, duration)} + onCancel={() => setActiveInsertionPoint(null)} + /> + ) : ( + setActiveInsertionPoint(task.id)} + isActive={activeInsertionPoint === task.id} + /> + )} + + + {/* Task Block with Time Label */} + + + + + + handleDeleteTask(task.id)} + onToggleLock={() => handleToggleLock(task.id)} + onDragStart={() => setDraggingTaskId(task.id)} + onDragEnd={() => setDraggingTaskId(null)} + isDragging={draggingTaskId === task.id} + /> + + + + {/* Insertion Point After Task */} + + {activeInsertionPoint === `${task.id}-after` ? ( + handleInsertTask(task.id, title, duration)} + onCancel={() => setActiveInsertionPoint(null)} + /> + ) : ( + setActiveInsertionPoint(`${task.id}-after`)} + isActive={activeInsertionPoint === `${task.id}-after`} + /> + )} + + + )) + )} + + {/* End Insertion Point */} + {tasks.length > 0 && ( + + {activeInsertionPoint === 'end' ? ( + { + const lastTask = tasks[tasks.length - 1] + handleInsertTask(lastTask.id, title, duration) + }} + onCancel={() => setActiveInsertionPoint(null)} + /> + ) : ( + setActiveInsertionPoint('end')} + isActive={activeInsertionPoint === 'end'} + /> + )} + + )} + + {/* Tip - Centered */} + + + Tip: Create tasks with [+] at top, then drag to reorder. + + + + + ) +} diff --git a/src/components/TaskBlock.tsx b/src/components/TaskBlock.tsx index 368188e..c992e2a 100644 --- a/src/components/TaskBlock.tsx +++ b/src/components/TaskBlock.tsx @@ -114,44 +114,43 @@ export const TaskBlock = ({ onMouseLeave={() => setIsHovered(false)} sx={{ position: 'relative', - background: `linear-gradient(90deg, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0) 70%), ${backgroundColor}`, - borderRadius: '16px', + background: `linear-gradient(135deg, rgba(255,255,255,0.4) 0%, rgba(255,255,255,0) 100%), ${backgroundColor}`, + borderRadius: '20px', // More rounded as per ref border: isBeingDragged - ? '2px dashed #000000' + ? '2px dashed rgba(0,0,0,0.1)' : task.isOverlapping - ? `2px solid ${colors.overlapBorder}` - : isDropTarget - ? '2px solid #000000' - : 'none', - padding: '18px', - marginBottom: '10px', + ? `2px solid ${colors.overlapBorder}` + : isDropTarget + ? '2px solid rgba(0,0,0,0.2)' + : '1px solid rgba(0,0,0,0.03)', + padding: '24px', // More padding minHeight: `${height}px`, cursor: task.isLocked ? 'not-allowed' : isBeingDragged ? 'grabbing' : 'grab', - opacity: isBeingDragged ? 0.85 : isDragging ? 0.85 : isPreview ? 0.6 : 1, - transition: 'box-shadow 150ms ease, opacity 150ms ease, border 150ms ease', + opacity: isBeingDragged ? 0.8 : isDragging ? 0.8 : isPreview ? 0.6 : 1, + transition: 'all 200ms cubic-bezier(0.4, 0, 0.2, 1)', boxShadow: isBeingDragged - ? '0 4px 12px rgba(0,0,0,0.15)' + ? '0 20px 40px rgba(0,0,0,0.12)' : showNewHighlight - ? '0 0 0 3px #4CAF50, 0 4px 12px rgba(76, 175, 80, 0.3)' - : '0 10px 26px rgba(0,0,0,0.10)', + ? '0 0 0 3px #4CAF50, 0 8px 20px rgba(76, 175, 80, 0.2)' + : '0 8px 24px rgba(0,0,0,0.06)', // Softer shadow '&:hover': { - boxShadow: task.isLocked || isBeingDragged ? '0 10px 26px rgba(0,0,0,0.10)' : '0 14px 32px rgba(0,0,0,0.14)', + boxShadow: task.isLocked || isBeingDragged ? '0 8px 24px rgba(0,0,0,0.06)' : '0 12px 32px rgba(0,0,0,0.1)', + transform: task.isLocked || isBeingDragged ? 'none' : 'translateY(-1px)', }, // Overlapping pulse animation animation: task.isOverlapping ? 'pulse 1.5s ease-in-out infinite' : showNewHighlight - ? 'newTaskGlow 0.5s ease-out' - : 'none', + ? 'newTaskGlow 0.5s ease-out' + : 'none', '@keyframes pulse': { '0%, 100%': { borderColor: colors.overlapBorder, opacity: 1 }, '50%': { borderColor: colors.overlapBorder, opacity: 0.6 }, }, '@keyframes newTaskGlow': { - '0%': { boxShadow: '0 0 0 6px #4CAF50, 0 8px 24px rgba(76, 175, 80, 0.5)' }, - '100%': { boxShadow: '0 0 0 3px #4CAF50, 0 4px 12px rgba(76, 175, 80, 0.3)' }, + '0%': { boxShadow: '0 0 0 6px #4CAF50, 0 8px 24px rgba(76, 175, 80, 0.4)' }, + '100%': { boxShadow: '0 0 0 3px #4CAF50, 0 4px 12px rgba(76, 175, 80, 0.2)' }, }, - // Overflow hidden for the highlight bar overflow: 'hidden', }} > @@ -159,12 +158,12 @@ export const TaskBlock = ({ {task.title} @@ -174,8 +173,9 @@ export const TaskBlock = ({ Est. {formatDuration(task.durationMinutes)} diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index ccbd6c0..99485a3 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -234,7 +234,14 @@ export const Timeline = () => { {/* Timeline */} - + void +} + +export const TodayView = ({ selectedDateKey, onDateChange }: TodayViewProps) => { + return ( + + + + ) +} diff --git a/src/theme/theme.ts b/src/theme/theme.ts index a429383..83b431c 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -1,12 +1,14 @@ import { createTheme } from '@mui/material/styles' -// Task block colors - alternating greens (pastel to forest) +// Task block colors - premium pastels (mint, sage, seafoam) export const taskColors = [ - '#E8F5E9', // Green 1: Pastel green (very light, soft) - '#C8E6C9', // Green 2: Light mint green - '#A5D6A7', // Green 3: Medium green - '#81C784', // Green 4: Medium-dark green - '#66BB6A', // Green 5: Forest green (richest) + '#E3F2FD', // Soft Blue/Cyan + '#E0F2F1', // Light Mint/Teal (Reference Card 1) + '#E8F5E9', // Pastel Green + '#F1F8E9', // Light Lime/Green + '#F9FBE7', // Soft Yellow/Green + '#C8E6C9', // Slightly deeper Mint (Reference Card 2) + '#B2DFDB', // Deeper Teal (Reference Card 3) ] // Special state colors diff --git a/src/utils/timeCalculations.ts b/src/utils/timeCalculations.ts index c83bd90..b6caa2d 100644 --- a/src/utils/timeCalculations.ts +++ b/src/utils/timeCalculations.ts @@ -277,6 +277,11 @@ export const getVisibleTimeRange = ( * Format time as "8:00 AM" or "2:30 PM" */ export const formatTime = (date: Date): string => { + if (!(date instanceof Date) || isNaN(date.getTime())) { + console.error('formatTime: Invalid date provided', date) + return 'Invalid Time' + } + let hours = date.getHours() const minutes = date.getMinutes() const ampm = hours >= 12 ? 'PM' : 'AM'