From 5a3ee5c7cb34b1388f6037c9dd265a25b103cd8e Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 26 Mar 2026 08:51:06 +0100 Subject: [PATCH] feat(todo): improve task UI with priority checkboxes, drag styling, and route fix - Integrate priority indicator into checkbox (color fill + border weight + dashed for low + ! for urgent) for better a11y - Reduce notepad border-radius and enable rounding on mobile - Fix reorder route being shadowed by :id param route - Improve drag & drop styling: remove container highlight, add lifted effect on dragged item Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/backend/src/task/task.controller.ts | 12 ++-- .../web/src/lib/components/TaskItem.svelte | 68 +++++++++++++------ .../web/src/lib/components/TaskList.svelte | 32 ++++++--- .../apps/web/src/routes/(app)/+page.svelte | 4 +- 4 files changed, 77 insertions(+), 39 deletions(-) diff --git a/apps/todo/apps/backend/src/task/task.controller.ts b/apps/todo/apps/backend/src/task/task.controller.ts index 1e3996d52..b2f9bf08f 100644 --- a/apps/todo/apps/backend/src/task/task.controller.ts +++ b/apps/todo/apps/backend/src/task/task.controller.ts @@ -58,6 +58,12 @@ export class TaskController { return { tasks }; } + @Put('reorder') + async reorder(@CurrentUser() user: CurrentUserData, @Body() dto: ReorderTasksDto) { + const tasks = await this.taskService.reorder(user.userId, dto.taskIds, dto.projectId); + return { tasks }; + } + @Get(':id') async findOne(@CurrentUser() user: CurrentUserData, @Param('id') id: string) { const task = await this.taskService.findByIdOrThrow(id, user.userId); @@ -119,10 +125,4 @@ export class TaskController { const task = await this.taskService.findByIdOrThrow(id, user.userId); return { task }; } - - @Put('reorder') - async reorder(@CurrentUser() user: CurrentUserData, @Body() dto: ReorderTasksDto) { - const tasks = await this.taskService.reorder(user.userId, dto.taskIds, dto.projectId); - return { tasks }; - } } diff --git a/apps/todo/apps/web/src/lib/components/TaskItem.svelte b/apps/todo/apps/web/src/lib/components/TaskItem.svelte index 61e0d4305..aba8ad8d4 100644 --- a/apps/todo/apps/web/src/lib/components/TaskItem.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskItem.svelte @@ -372,11 +372,12 @@ - + - -
-
{#if isEditingTitle} @@ -806,21 +803,13 @@ pointer-events: none; } - /* Priority dot */ - .priority-dot { - width: 0.625rem; - height: 0.625rem; - border-radius: 9999px; - flex-shrink: 0; - } - - /* Checkbox */ + /* Checkbox with priority color fill */ .task-checkbox { width: 1.25rem; height: 1.25rem; border-radius: 9999px; - border: 2px solid rgba(0, 0, 0, 0.2); - background: transparent; + border: 2px solid var(--priority-color, rgba(0, 0, 0, 0.2)); + background: color-mix(in srgb, var(--priority-color) 15%, transparent); display: flex; align-items: center; justify-content: center; @@ -831,22 +820,57 @@ } :global(.dark) .task-checkbox { - border-color: rgba(255, 255, 255, 0.3); + border-color: var(--priority-color, rgba(255, 255, 255, 0.3)); + background: color-mix(in srgb, var(--priority-color) 20%, transparent); } .task-checkbox:hover { - border-color: #8b5cf6; - background: rgba(139, 92, 246, 0.1); + background: color-mix(in srgb, var(--priority-color) 35%, transparent); + } + + /* Priority: low — dashed thin border */ + .task-checkbox.priority-low { + border-style: dashed; + border-width: 1.5px; + } + + /* Priority: medium — normal solid border */ + .task-checkbox.priority-medium { + border-width: 2px; + } + + /* Priority: high — thick border */ + .task-checkbox.priority-high { + border-width: 3px; + } + + /* Priority: urgent — thick border + exclamation mark */ + .task-checkbox.priority-urgent { + border-width: 3px; + } + + .priority-bang { + font-size: 0.6875rem; + font-weight: 800; + line-height: 1; + color: var(--priority-color); + pointer-events: none; } .task-checkbox.checked { background: #8b5cf6; border-color: #8b5cf6; + border-style: solid; + } + + .task-checkbox.checked .priority-bang { + display: none; } .task-checkbox.animating { background: #22c55e; border-color: #22c55e; + border-style: solid; transform: scale(1.2); } diff --git a/apps/todo/apps/web/src/lib/components/TaskList.svelte b/apps/todo/apps/web/src/lib/components/TaskList.svelte index 026d317c0..a9e7522ad 100644 --- a/apps/todo/apps/web/src/lib/components/TaskList.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskList.svelte @@ -243,7 +243,7 @@ items, flipDurationMs, dropTargetStyle: {}, - dropTargetClasses: ['task-drop-target'], + dropTargetClasses: [], type: 'homepage-tasks', }} onconsider={handleDndConsider} @@ -334,17 +334,31 @@ opacity: 0.5; } - :global(.task-drop-target) { - outline: 2px dashed #8b5cf6 !important; - outline-offset: -2px; - background: rgba(139, 92, 246, 0.08) !important; + /* Dragged item styling */ + :global([aria-grabbed='true']) { + opacity: 0.9; + transform: scale(1.02); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + border-radius: 0.5rem; + background: rgba(255, 255, 255, 0.95); + z-index: 100; } - :global(.task-drop-target) .empty-placeholder { - opacity: 0; + :global(.dark [aria-grabbed='true']) { + background: rgba(40, 40, 40, 0.95); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); } - :global(.dark .task-drop-target) { - background: rgba(139, 92, 246, 0.15) !important; + /* Shadow placeholder (where dragged item will land) */ + :global(.task-list [data-is-dnd-shadow-item-hint]) { + background: rgba(139, 92, 246, 0.06); + border-radius: 0.375rem; + border: 1px dashed rgba(139, 92, 246, 0.3); + visibility: visible !important; + } + + :global(.dark .task-list [data-is-dnd-shadow-item-hint]) { + background: rgba(139, 92, 246, 0.1); + border-color: rgba(139, 92, 246, 0.4); } diff --git a/apps/todo/apps/web/src/routes/(app)/+page.svelte b/apps/todo/apps/web/src/routes/(app)/+page.svelte index a5959ee05..6945b6b44 100644 --- a/apps/todo/apps/web/src/routes/(app)/+page.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+page.svelte @@ -507,7 +507,7 @@ max-width: 560px; margin: 0 auto; background: #fffef5; - border-radius: 0.75rem; + border-radius: 0.5rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); @@ -529,7 +529,7 @@ @media (max-width: 640px) { .notepad { max-width: 100%; - border-radius: 0; + border-radius: 0.5rem; } }