mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 04:53:38 +02:00
feat(command-bar): match priority highlight colors to UI
Priority keywords now show their actual UI colors: - Dringend (urgent): red #ef4444 - Wichtig (high): orange #f97316 - Normal (medium): yellow #eab308 - Später (low): green #22c55e 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d9626a9d8f
commit
aa117c51cd
12 changed files with 551 additions and 92 deletions
|
|
@ -59,6 +59,13 @@
|
|||
</script>
|
||||
|
||||
<div class="task-item group" class:completed={task.isCompleted}>
|
||||
<!-- Drag handle -->
|
||||
<div class="drag-handle">
|
||||
<svg class="drag-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Priority indicator -->
|
||||
<div
|
||||
class="priority-dot"
|
||||
|
|
@ -80,27 +87,9 @@
|
|||
{task.title}
|
||||
</span>
|
||||
|
||||
<!-- Meta info inline -->
|
||||
{#if dueDateText() || subtaskProgress() || (task.labels && task.labels.length > 0)}
|
||||
<!-- Labels and subtasks below title -->
|
||||
{#if subtaskProgress() || (task.labels && task.labels.length > 0)}
|
||||
<div class="task-meta">
|
||||
{#if dueDateText()}
|
||||
<span
|
||||
class="meta-item date"
|
||||
class:overdue={isOverdue()}
|
||||
class:today={isToday(new Date(task.dueDate || 0))}
|
||||
>
|
||||
<svg class="meta-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{dueDateText()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if subtaskProgress()}
|
||||
<span class="meta-item">
|
||||
<svg class="meta-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -129,6 +118,17 @@
|
|||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Due date (always on the right) -->
|
||||
{#if dueDateText()}
|
||||
<span
|
||||
class="due-date"
|
||||
class:overdue={isOverdue()}
|
||||
class:today={task.dueDate && isToday(new Date(task.dueDate))}
|
||||
>
|
||||
{dueDateText()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Project indicator -->
|
||||
{#if projectColor()}
|
||||
<div class="project-dot" style="background-color: {projectColor()}"></div>
|
||||
|
|
@ -151,18 +151,16 @@
|
|||
.task-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 9999px;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
:global(.dark) .task-item {
|
||||
|
|
@ -172,11 +170,8 @@
|
|||
|
||||
.task-item:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
:global(.dark) .task-item:hover {
|
||||
|
|
@ -188,6 +183,43 @@
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Drag handle */
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem;
|
||||
margin-left: -0.25rem;
|
||||
}
|
||||
|
||||
.task-item:hover .drag-handle {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
/* During drag, disable pointer events on interactive elements */
|
||||
:global([aria-grabbed='true']) .task-checkbox,
|
||||
:global([aria-grabbed='true']) .task-content,
|
||||
:global([aria-grabbed='true']) .delete-btn {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Priority dot */
|
||||
.priority-dot {
|
||||
width: 0.5rem;
|
||||
|
|
@ -284,14 +316,6 @@
|
|||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.meta-item.date.overdue {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.meta-item.date.today {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
|
|
@ -306,6 +330,26 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Due date */
|
||||
.due-date {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:global(.dark) .due-date {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.due-date.overdue {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.due-date.today {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
/* Project dot */
|
||||
.project-dot {
|
||||
width: 0.5rem;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
|
||||
import type { Task } from '@todo/shared';
|
||||
import TaskItem from './TaskItem.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
|
|
@ -6,10 +7,55 @@
|
|||
interface Props {
|
||||
tasks: Task[];
|
||||
showCompleted?: boolean;
|
||||
enableDragDrop?: boolean;
|
||||
dropTargetDate?: Date | 'completed' | 'overdue';
|
||||
onEditTask?: (task: Task) => void;
|
||||
onTaskDrop?: (taskId: string, targetDate: Date | 'completed' | 'overdue') => void;
|
||||
}
|
||||
|
||||
let { tasks, showCompleted = false, onEditTask }: Props = $props();
|
||||
let {
|
||||
tasks,
|
||||
showCompleted = false,
|
||||
enableDragDrop = false,
|
||||
dropTargetDate,
|
||||
onEditTask,
|
||||
onTaskDrop,
|
||||
}: Props = $props();
|
||||
|
||||
// Local mutable state for dnd-zone
|
||||
let items = $state<Task[]>([]);
|
||||
|
||||
// Track last known tasks reference to detect parent updates
|
||||
let lastTasksRef: Task[] | null = null;
|
||||
|
||||
// Sync items with tasks only when tasks array reference changes
|
||||
$effect.pre(() => {
|
||||
if (tasks !== lastTasksRef) {
|
||||
items = [...tasks];
|
||||
lastTasksRef = tasks;
|
||||
}
|
||||
});
|
||||
|
||||
const flipDurationMs = 200;
|
||||
|
||||
function handleDndConsider(e: CustomEvent<{ items: Task[] }>) {
|
||||
items = e.detail.items;
|
||||
}
|
||||
|
||||
function handleDndFinalize(e: CustomEvent<{ items: Task[]; info: { id: string } }>) {
|
||||
const newItems = e.detail.items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID);
|
||||
const movedTaskId = e.detail.info.id;
|
||||
|
||||
// Check if this task came from another list (dropped INTO this list)
|
||||
const wasInThisList = tasks.some((t) => t.id === movedTaskId);
|
||||
|
||||
if (!wasInThisList && dropTargetDate && onTaskDrop) {
|
||||
// Task moved FROM another section TO this section
|
||||
onTaskDrop(movedTaskId, dropTargetDate);
|
||||
}
|
||||
|
||||
items = newItems;
|
||||
}
|
||||
|
||||
async function handleToggleComplete(task: Task) {
|
||||
if (task.isCompleted) {
|
||||
|
|
@ -24,14 +70,86 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="task-list">
|
||||
{#each tasks as task (task.id)}
|
||||
<TaskItem
|
||||
{task}
|
||||
{showCompleted}
|
||||
onToggleComplete={() => handleToggleComplete(task)}
|
||||
onDelete={() => handleDelete(task.id)}
|
||||
onEdit={onEditTask ? () => onEditTask(task) : undefined}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#if enableDragDrop}
|
||||
<div
|
||||
class="task-list"
|
||||
class:empty={items.length === 0}
|
||||
use:dndzone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
dropTargetClasses: ['task-drop-target'],
|
||||
type: 'homepage-tasks',
|
||||
}}
|
||||
onconsider={handleDndConsider}
|
||||
onfinalize={handleDndFinalize}
|
||||
>
|
||||
{#each items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID) as task (task.id)}
|
||||
<TaskItem
|
||||
{task}
|
||||
{showCompleted}
|
||||
onToggleComplete={() => handleToggleComplete(task)}
|
||||
onDelete={() => handleDelete(task.id)}
|
||||
onEdit={onEditTask ? () => onEditTask(task) : undefined}
|
||||
/>
|
||||
{/each}
|
||||
{#if items.length === 0}
|
||||
<div class="empty-placeholder">
|
||||
<span>Aufgabe hierher ziehen</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="task-list">
|
||||
{#each tasks as task (task.id)}
|
||||
<TaskItem
|
||||
{task}
|
||||
{showCompleted}
|
||||
onToggleComplete={() => handleToggleComplete(task)}
|
||||
onDelete={() => handleDelete(task.id)}
|
||||
onEdit={onEditTask ? () => onEditTask(task) : undefined}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.task-list {
|
||||
min-height: 60px;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.task-list.empty {
|
||||
border: 2px dashed rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:global(.dark) .task-list.empty {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.empty-placeholder {
|
||||
color: var(--color-muted-foreground, #9ca3af);
|
||||
font-size: 0.75rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(.task-drop-target) {
|
||||
outline: 2px dashed #8b5cf6 !important;
|
||||
outline-offset: -2px;
|
||||
background: rgba(139, 92, 246, 0.08) !important;
|
||||
}
|
||||
|
||||
:global(.task-drop-target) .empty-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:global(.dark .task-drop-target) {
|
||||
background: rgba(139, 92, 246, 0.15) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ export const tasksStore = {
|
|||
* Update task optimistically (for drag and drop)
|
||||
* Updates local state immediately, then syncs with server
|
||||
*/
|
||||
updateTaskOptimistic(
|
||||
async updateTaskOptimistic(
|
||||
id: string,
|
||||
data: {
|
||||
dueDate?: string | null;
|
||||
|
|
@ -255,32 +255,25 @@ export const tasksStore = {
|
|||
|
||||
tasks = tasks.map((t) => (t.id === id ? { ...t, ...data } : t));
|
||||
|
||||
// Sync with server in background
|
||||
if (data.isCompleted !== undefined) {
|
||||
const apiCall = data.isCompleted ? tasksApi.completeTask(id) : tasksApi.uncompleteTask(id);
|
||||
try {
|
||||
// Handle completion state change first
|
||||
if (data.isCompleted !== undefined && data.isCompleted !== originalTask.isCompleted) {
|
||||
if (data.isCompleted) {
|
||||
await tasksApi.completeTask(id);
|
||||
} else {
|
||||
await tasksApi.uncompleteTask(id);
|
||||
}
|
||||
}
|
||||
|
||||
apiCall
|
||||
.then((updatedTask) => {
|
||||
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
|
||||
})
|
||||
.catch((e) => {
|
||||
// Rollback on error
|
||||
console.error('Failed to update task:', e);
|
||||
tasks = tasks.map((t) => (t.id === id ? originalTask : t));
|
||||
});
|
||||
}
|
||||
|
||||
if (data.dueDate !== undefined) {
|
||||
tasksApi
|
||||
.updateTask(id, { dueDate: data.dueDate })
|
||||
.then((updatedTask) => {
|
||||
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
|
||||
})
|
||||
.catch((e) => {
|
||||
// Rollback on error
|
||||
console.error('Failed to update task:', e);
|
||||
tasks = tasks.map((t) => (t.id === id ? originalTask : t));
|
||||
});
|
||||
// Handle due date change
|
||||
if (data.dueDate !== undefined) {
|
||||
const updatedTask = await tasksApi.updateTask(id, { dueDate: data.dueDate });
|
||||
tasks = tasks.map((t) => (t.id === id ? updatedTask : t));
|
||||
}
|
||||
} catch (e) {
|
||||
// Rollback on error
|
||||
console.error('Failed to update task:', e);
|
||||
tasks = tasks.map((t) => (t.id === id ? originalTask : t));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue