feat: priority view resize handle + dense task cards with actions

Resize handle:
- Drag the divider between priority panel and calendar to resize it
- Height persists in cookie (priorityViewHeight)

Dense task cards:
- Single-row compact layout: [✓] date · title · action icons
- Date shown before the title
- Action icon bar: rename (✏), note (📝), link (🔗), project (📁), delegate (👤), remove (×)
- Inline title editing: double-click or click ✏ icon
- Inline note textarea: toggles on 📝 click, saves on blur
- Inline URL input with open-link button: toggles on 🔗 click
- Inline project selector: toggles on 📁 click
- Icons colored when field has a value (note, url, project, delegate)
- Export/import now round-trips url, kanbanStage, recurrenceDays fields

v1.88.0
This commit is contained in:
mARTin 2026-04-06 12:25:05 +02:00
parent 8ffc127671
commit 149f0cf021
3 changed files with 213 additions and 118 deletions

View File

@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
"version": "1.87.2",
"version": "1.88.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {

View File

@ -24,6 +24,10 @@ import {
CalendarDays,
FolderOpen,
List,
Pencil,
FileText,
Link2,
ExternalLink,
} from "lucide-react";
export interface PriorityTask {
@ -43,6 +47,7 @@ export interface PriorityTask {
dayOfWeek?: number | null;
markdownContent?: string | null;
startTime?: string | null;
url?: string | null;
}
export interface PrioritySomedayList {
@ -385,6 +390,8 @@ export default function PriorityView({
onClear={clearEisenhower}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
@ -402,6 +409,8 @@ export default function PriorityView({
onSetGrade={setAbcde}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
@ -419,6 +428,8 @@ export default function PriorityView({
onToggle={toggleIvyLee}
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
@ -436,6 +447,7 @@ export default function PriorityView({
onDelegate={openDelegate}
onToggleComplete={toggleComplete}
onUpdateTask={onUpdateTask}
projects={projects}
cardBg={cardBg}
border={border}
textPrimary={textPrimary}
@ -565,76 +577,161 @@ interface TaskCardProps {
textSecondary: string;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask?: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects?: PriorityProject[];
onRemove?: () => void;
children?: React.ReactNode;
compact?: boolean;
de?: boolean;
}
function TaskCard({ task, darkMode, cardBg, border, textPrimary, textSecondary, onDelegate, onToggleComplete, children, compact }: TaskCardProps) {
const hasDelegate = !!task.delegatedTo;
function TaskCard({ task, darkMode, cardBg, border, textPrimary, textSecondary, onDelegate, onToggleComplete, onUpdateTask, projects, onRemove, children, de }: TaskCardProps) {
const [editingTitle, setEditingTitle] = useState(false);
const [titleVal, setTitleVal] = useState(task.title);
const [showNote, setShowNote] = useState(false);
const [noteVal, setNoteVal] = useState(task.markdownContent || "");
const [showUrl, setShowUrl] = useState(false);
const [urlVal, setUrlVal] = useState(task.url || "");
const [showProject, setShowProject] = useState(false);
const saveTitle = () => {
if (onUpdateTask && titleVal.trim() && titleVal.trim() !== task.title) {
onUpdateTask(task.id, { title: titleVal.trim() });
}
setEditingTitle(false);
};
const iconBtn = (onClick: (e: React.MouseEvent) => void, title: string, active: boolean, activeColor: string, icon: React.ReactNode) => (
<button
onClick={(e) => { e.stopPropagation(); onClick(e); }}
title={title}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px 3px", borderRadius: "3px", color: active ? activeColor : "#9ca3af", flexShrink: 0, display: "flex", alignItems: "center" }}
>
{icon}
</button>
);
return (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData("text/plain", task.id);
e.dataTransfer.effectAllowed = "move";
}}
onDragStart={(e) => { e.dataTransfer.setData("text/plain", task.id); e.dataTransfer.effectAllowed = "move"; }}
style={{
background: task.completed ? (darkMode ? "#111827" : "#f9fafb") : cardBg,
border: `1px solid ${border}`,
borderRadius: "8px",
padding: compact ? "7px 10px" : "10px 12px",
display: "flex",
alignItems: "flex-start",
gap: "8px",
borderRadius: "7px",
padding: "4px 6px",
opacity: task.completed ? 0.6 : 1,
transition: "opacity 0.15s",
cursor: "grab",
display: "flex",
flexDirection: "column",
gap: "2px",
}}
>
{/* Checkbox */}
<button
onClick={() => onToggleComplete(task)}
style={{ background: "none", border: "none", cursor: "pointer", padding: 0, marginTop: "1px", flexShrink: 0 }}
title="Toggle complete"
>
{task.completed
? <Check size={16} style={{ color: "#059669" }} />
: <Circle size={16} style={{ color: "#9ca3af" }} />
}
</button>
{/* Body */}
<div style={{ flex: 1, minWidth: 0 }}>
<span style={{ fontSize: "0.88rem", color: textPrimary, fontWeight: 500, textDecoration: task.completed ? "line-through" : "none", wordBreak: "break-word" }}>
{task.title}
</span>
<div style={{ display: "flex", flexWrap: "wrap", gap: "5px", marginTop: "4px" }}>
{task.scheduledDate && (
<span style={{ fontSize: "0.7rem", color: "#6b7280", background: darkMode ? "#374151" : "#f3f4f6", borderRadius: "4px", padding: "1px 5px" }}>
{formatDate(task.scheduledDate)}
</span>
)}
{/* Main row */}
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
{/* Complete toggle */}
<button
onClick={() => onToggleComplete(task)}
style={{ background: "none", border: "none", cursor: "pointer", padding: 0, flexShrink: 0, display: "flex", alignItems: "center" }}
>
{task.completed ? <Check size={13} style={{ color: "#059669" }} /> : <Circle size={13} style={{ color: "#9ca3af" }} />}
</button>
{/* Date */}
{task.scheduledDate && (
<span style={{ fontSize: "0.63rem", color: "#6b7280", flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>
{formatDate(task.scheduledDate)}
</span>
)}
{/* Title */}
{editingTitle ? (
<input
value={titleVal}
onChange={(e) => setTitleVal(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") saveTitle(); if (e.key === "Escape") { setTitleVal(task.title); setEditingTitle(false); } }}
onBlur={saveTitle}
autoFocus
onClick={(e) => e.stopPropagation()}
style={{ flex: 1, fontSize: "0.82rem", fontWeight: 500, border: `1px solid ${border}`, borderRadius: "4px", padding: "1px 4px", background: darkMode ? "#374151" : "#fff", color: textPrimary, outline: "none", minWidth: 0 }}
/>
) : (
<span
style={{ flex: 1, fontSize: "0.82rem", fontWeight: 500, color: textPrimary, textDecoration: task.completed ? "line-through" : "none", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}
title={task.title}
onDoubleClick={() => setEditingTitle(true)}
>
{task.title}
</span>
)}
{/* Action icons */}
{iconBtn(() => setEditingTitle(true), de ? "Umbenennen" : "Rename", false, "#6366f1", <Pencil size={11} />)}
{iconBtn(() => { setShowNote((v) => !v); if (!showNote) setNoteVal(task.markdownContent || ""); }, de ? "Notiz" : "Note", !!task.markdownContent || showNote, "#6366f1", <FileText size={11} />)}
{iconBtn(() => { setShowUrl((v) => !v); if (!showUrl) setUrlVal(task.url || ""); }, "Link", !!task.url || showUrl, "#3b82f6", <Link2 size={11} />)}
{projects && projects.length > 0 && iconBtn(() => setShowProject((v) => !v), de ? "Projekt" : "Project", !!task.projectId || showProject, "#059669", <FolderOpen size={11} />)}
{iconBtn(() => onDelegate(task), de ? "Delegieren" : "Delegate", !!task.delegatedTo, "#8b5cf6", <UserCheck size={11} />)}
{onRemove && iconBtn(() => onRemove(), de ? "Entfernen" : "Remove", false, "#ef4444", <X size={11} />)}
</div>
{/* Badges */}
{(task.project || task.delegatedTo) && (
<div style={{ display: "flex", gap: "3px", paddingLeft: "17px", flexWrap: "wrap" }}>
{task.project && (
<span style={{ fontSize: "0.7rem", color: task.project.color || "#6366f1", background: `${task.project.color || "#6366f1"}20`, borderRadius: "4px", padding: "1px 5px" }}>
<span style={{ fontSize: "0.62rem", color: task.project.color || "#6366f1", background: `${task.project.color || "#6366f1"}20`, borderRadius: "3px", padding: "0 4px" }}>
{task.project.name}
</span>
)}
{hasDelegate && (
<span style={{ fontSize: "0.7rem", color: "#8b5cf6", background: "#8b5cf620", borderRadius: "4px", padding: "1px 5px", display: "flex", alignItems: "center", gap: "3px" }}>
{task.delegatedTo === "AI" ? <Sparkles size={9} /> : <User size={9} />}
{task.delegatedTo && (
<span style={{ fontSize: "0.62rem", color: "#8b5cf6", background: "#8b5cf620", borderRadius: "3px", padding: "0 4px", display: "flex", alignItems: "center", gap: "2px" }}>
{task.delegatedTo === "AI" ? <Sparkles size={8} /> : <User size={8} />}
{task.delegatedTo}
</span>
)}
</div>
{children}
</div>
{/* Delegate button */}
<button
onClick={() => onDelegate(task)}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px 4px", borderRadius: "5px", color: hasDelegate ? "#8b5cf6" : "#9ca3af", flexShrink: 0 }}
title="Delegate"
>
<UserCheck size={14} />
</button>
)}
{/* Note editor */}
{showNote && (
<textarea
value={noteVal}
onChange={(e) => setNoteVal(e.target.value)}
onBlur={() => { if (onUpdateTask) onUpdateTask(task.id, { markdownContent: noteVal || null }); }}
placeholder={de ? "Notiz..." : "Note..."}
rows={2}
style={{ fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "4px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, resize: "vertical", fontFamily: "inherit", outline: "none", width: "100%", boxSizing: "border-box", marginTop: "2px" }}
/>
)}
{/* URL input */}
{showUrl && (
<div style={{ display: "flex", gap: "4px", alignItems: "center", marginTop: "2px" }}>
<input
value={urlVal}
onChange={(e) => setUrlVal(e.target.value)}
onBlur={() => { if (onUpdateTask) onUpdateTask(task.id, { url: urlVal || null }); }}
onKeyDown={(e) => { if (e.key === "Enter" && onUpdateTask) { onUpdateTask(task.id, { url: urlVal || null }); setShowUrl(false); } }}
placeholder="https://..."
style={{ flex: 1, fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "3px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, outline: "none" }}
/>
{task.url && (
<a href={task.url} target="_blank" rel="noopener noreferrer" style={{ color: "#3b82f6", flexShrink: 0, display: "flex" }}>
<ExternalLink size={12} />
</a>
)}
</div>
)}
{/* Project selector */}
{showProject && projects && (
<select
value={task.projectId || ""}
onChange={(e) => { if (onUpdateTask) { onUpdateTask(task.id, { projectId: e.target.value || null }); setShowProject(false); } }}
style={{ fontSize: "0.78rem", border: `1px solid ${border}`, borderRadius: "5px", padding: "3px 6px", background: darkMode ? "#374151" : "#f9fafb", color: textPrimary, outline: "none", width: "100%", cursor: "pointer", marginTop: "2px" }}
>
<option value="">{de ? "Kein Projekt" : "No project"}</option>
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
)}
{/* Children (quick-assign buttons for unset/ungraded sections) */}
{children}
</div>
);
}
@ -655,6 +752,8 @@ interface EisenhowerProps {
onClear: (t: PriorityTask) => void;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
@ -668,7 +767,7 @@ const QUADRANT_CONFIG = [
{ urgency: false, importance: false, label: "Delete", labelDe: "Eliminieren", desc: "Neither", descDe: "Weder noch", color: "#6b7280", bgLight: "#f9fafb", bgDark: "#1f2937", icon: <Trash2 size={14} /> },
];
function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSetQuadrant, onClear, onDelegate, onToggleComplete, cardBg, border, textPrimary, textSecondary }: EisenhowerProps) {
function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSetQuadrant, onClear, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: EisenhowerProps) {
const quadrantTasks = [q1, q2, q3, q4];
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
const [dragOver, setDragOver] = useState<number | null>(null);
@ -730,11 +829,7 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
</p>
)}
{tasks.map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} compact>
<button onClick={() => onClear(t)} style={{ fontSize: "0.65rem", marginTop: "3px", background: "none", border: "none", color: textSecondary, cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}>
<X size={9} /> {de ? "Entfernen" : "Remove"}
</button>
</TaskCard>
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onClear(t)} de={de} />
))}
</div>
)}
@ -758,13 +853,13 @@ function EisenhowerMatrix({ q1, q2, q3, q4, unset, allTasks, darkMode, de, onSet
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{unset.map((t) => (
<div key={t.id}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
<div style={{ display: "flex", gap: "4px", marginTop: "6px", flexWrap: "wrap" }}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
<div style={{ display: "flex", gap: "4px", marginTop: "5px", flexWrap: "wrap" }}>
{QUADRANT_CONFIG.map((qc, i) => (
<button
key={i}
onClick={() => onSetQuadrant(t, qc.urgency, qc.importance)}
style={{ fontSize: "0.68rem", padding: "2px 7px", borderRadius: "5px", border: `1px solid ${qc.color}`, background: `${qc.color}18`, color: qc.color, cursor: "pointer", display: "flex", alignItems: "center", gap: "3px", fontWeight: 600 }}
style={{ fontSize: "0.65rem", padding: "2px 6px", borderRadius: "4px", border: `1px solid ${qc.color}`, background: `${qc.color}18`, color: qc.color, cursor: "pointer", display: "flex", alignItems: "center", gap: "3px", fontWeight: 600 }}
>
{qc.icon} {de ? qc.labelDe : qc.label}
</button>
@ -791,13 +886,15 @@ interface AbcdeViewProps {
onSetGrade: (t: PriorityTask, grade: string | null) => void;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
}
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, cardBg, border, textPrimary, textSecondary }: AbcdeViewProps) {
function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: AbcdeViewProps) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({ none: true });
const [dragOver, setDragOver] = useState<string | null>(null);
@ -858,25 +955,7 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
</p>
)}
{tasks.map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
<div style={{ display: "flex", gap: "3px", marginTop: "6px", flexWrap: "wrap" }}>
{gradeList.filter((g) => g !== grade).map((g) => (
<button
key={g}
onClick={() => onSetGrade(t, g)}
style={{ fontSize: "0.68rem", padding: "1px 7px", borderRadius: "5px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
>
{g}
</button>
))}
<button
onClick={() => onSetGrade(t, null)}
style={{ fontSize: "0.68rem", padding: "1px 7px", borderRadius: "5px", border: `1px solid ${textSecondary}`, background: "transparent", color: textSecondary, cursor: "pointer" }}
>
<X size={9} />
</button>
</div>
</TaskCard>
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onSetGrade(t, null)} de={de} />
))}
</div>
)}
@ -904,13 +983,13 @@ function AbcdeView({ groups, allTasks, darkMode, de, onSetGrade, onDelegate, onT
{!collapsed.none && (
<div style={{ padding: "10px", display: "flex", flexDirection: "column", gap: "6px" }}>
{(groups.none || []).map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
<div style={{ display: "flex", gap: "3px", marginTop: "6px", flexWrap: "wrap" }}>
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
<div style={{ display: "flex", gap: "3px", marginTop: "5px", flexWrap: "wrap" }}>
{gradeList.map((g) => (
<button
key={g}
onClick={() => onSetGrade(t, g)}
style={{ fontSize: "0.68rem", padding: "1px 7px", borderRadius: "5px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
style={{ fontSize: "0.65rem", padding: "1px 6px", borderRadius: "4px", border: `1px solid ${ABCDE_LABELS[g].color}`, background: `${ABCDE_LABELS[g].color}18`, color: ABCDE_LABELS[g].color, cursor: "pointer", fontWeight: 700 }}
>
{g}
</button>
@ -937,13 +1016,15 @@ interface IvyLeeViewProps {
onToggle: (id: string) => void;
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
}
function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, cardBg, border, textPrimary, textSecondary }: IvyLeeViewProps) {
function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: IvyLeeViewProps) {
const accentColor = "#6366f1";
const selectedTasks = tasks.filter((t) => selected.has(t.id));
const unselected = tasks.filter((t) => !selected.has(t.id));
@ -998,11 +1079,7 @@ function IvyLeeView({ tasks, selected, darkMode, de, onToggle, onDelegate, onTog
<div key={t.id} style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<span style={{ fontSize: "0.78rem", fontWeight: 700, color: accentColor, width: "16px", textAlign: "center", flexShrink: 0 }}>{i + 1}</span>
<div style={{ flex: 1 }}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} compact>
<button onClick={() => onToggle(t.id)} style={{ fontSize: "0.65rem", background: "none", border: "none", color: "#ef4444", cursor: "pointer", padding: 0, marginTop: "2px", display: "flex", alignItems: "center", gap: "2px" }}>
<X size={9} /> {de ? "Entfernen" : "Remove"}
</button>
</TaskCard>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onToggle(t.id)} de={de} />
</div>
</div>
);
@ -1067,13 +1144,14 @@ interface ParetoViewProps {
onDelegate: (t: PriorityTask) => void;
onToggleComplete: (t: PriorityTask) => void;
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
projects: PriorityProject[];
cardBg: string;
border: string;
textPrimary: string;
textSecondary: string;
}
function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, cardBg, border, textPrimary, textSecondary }: ParetoViewProps) {
function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplete, onUpdateTask, projects, cardBg, border, textPrimary, textSecondary }: ParetoViewProps) {
const [showRest, setShowRest] = useState(false);
const total = important.length + rest.length;
const pct = total > 0 ? Math.round((important.length / total) * 100) : 0;
@ -1118,16 +1196,7 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
)}
{important.map((t) => (
<div key={t.id} style={{ borderLeft: "3px solid #f97316", borderRadius: "0 8px 8px 0" }}>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
<div style={{ display: "flex", gap: "4px", marginTop: "5px" }}>
<button
onClick={() => onUpdateTask(t.id, { importance: false })}
style={{ fontSize: "0.65rem", background: "none", border: "none", color: textSecondary, cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}
>
<X size={9} /> {de ? "Aus Fokus" : "Remove focus"}
</button>
</div>
</TaskCard>
<TaskCard task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} onRemove={() => onUpdateTask(t.id, { importance: false })} de={de} />
</div>
))}
</div>
@ -1147,7 +1216,7 @@ function ParetoView({ important, rest, darkMode, de, onDelegate, onToggleComplet
{showRest && (
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
{rest.map((t) => (
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete}>
<TaskCard key={t.id} task={t} darkMode={darkMode} cardBg={cardBg} border={border} textPrimary={textPrimary} textSecondary={textSecondary} onDelegate={onDelegate} onToggleComplete={onToggleComplete} onUpdateTask={onUpdateTask} projects={projects} de={de}>
<button
onClick={() => onUpdateTask(t.id, { importance: true })}
style={{ fontSize: "0.65rem", marginTop: "3px", background: "none", border: "none", color: "#f97316", cursor: "pointer", padding: 0, display: "flex", alignItems: "center", gap: "2px" }}

View File

@ -626,13 +626,22 @@ export default function WeeklyView() {
}
return null;
});
const resizingRef = useRef<{ target: 'someday' | 'allday'; startY: number; startHeight: number; handleOnTop: boolean } | null>(null);
const [priorityViewHeight, setPriorityViewHeight] = useState<number>(() => {
if (typeof document !== 'undefined') {
const c = document.cookie.match(/priorityViewHeight=(\d+)/);
return c ? parseInt(c[1]) : 340;
}
return 340;
});
const priorityViewRef = useRef<HTMLDivElement | null>(null);
const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday', handleOnTop = false) => {
const resizingRef = useRef<{ target: 'someday' | 'allday' | 'priority'; startY: number; startHeight: number; handleOnTop: boolean } | null>(null);
const startResize = useCallback((e: React.MouseEvent | React.TouchEvent, target: 'someday' | 'allday' | 'priority', handleOnTop = false) => {
e.preventDefault();
e.stopPropagation();
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
const section = target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
const section = target === 'someday' ? somedaySectionRef.current : target === 'priority' ? priorityViewRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
if (!section) return;
resizingRef.current = { target, startY: clientY, startHeight: section.getBoundingClientRect().height, handleOnTop };
@ -642,16 +651,21 @@ export default function WeeklyView() {
const rawDelta = y - resizingRef.current.startY;
// Top handle: dragging up = increase height (invert delta); bottom handle: normal
const delta = resizingRef.current.handleOnTop ? -rawDelta : rawDelta;
const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta));
if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight);
else setAllDayHeight(newHeight);
if (resizingRef.current.target === 'priority') {
setPriorityViewHeight(Math.max(80, Math.min(800, resizingRef.current.startHeight + delta)));
} else {
const newHeight = Math.max(40, Math.min(600, resizingRef.current.startHeight + delta));
if (resizingRef.current.target === 'someday') setSomedayHeight(newHeight);
else setAllDayHeight(newHeight);
}
};
const onEnd = () => {
if (resizingRef.current) {
const section2 = resizingRef.current.target === 'someday' ? somedaySectionRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
const cookieKey = resizingRef.current.target === 'someday' ? 'somedayHeight' : resizingRef.current.target === 'priority' ? 'priorityViewHeight' : 'allDayHeight';
const section2 = resizingRef.current.target === 'someday' ? somedaySectionRef.current : resizingRef.current.target === 'priority' ? priorityViewRef.current : (document.querySelector('.all-day-events-section') as HTMLElement);
if (section2) {
const h = Math.round(section2.getBoundingClientRect().height);
document.cookie = `${resizingRef.current.target === 'someday' ? 'somedayHeight' : 'allDayHeight'}=${h};path=/;max-age=31536000`;
document.cookie = `${cookieKey}=${h};path=/;max-age=31536000`;
}
}
resizingRef.current = null;
@ -6122,16 +6136,28 @@ export default function WeeklyView() {
{/* Priority View */}
{profile.viewStyle === "priority" && (
<div style={{ flex: 1, overflowY: "auto" }}>
<PriorityView
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
somedayLists={somedayLists.map(l => ({ id: l.id, title: l.title }))}
projects={projects}
darkMode={darkMode}
language={profile.language}
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
/>
</div>
<>
<div
ref={priorityViewRef}
style={{ height: `${priorityViewHeight}px`, overflowY: "auto", flexShrink: 0 }}
>
<PriorityView
tasks={[...tasks, ...somedayLists.flatMap(l => l.tasks)]}
somedayLists={somedayLists.map(l => ({ id: l.id, title: l.title }))}
projects={projects}
darkMode={darkMode}
language={profile.language}
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
/>
</div>
<div
className="resize-handle"
onMouseDown={(e) => startResize(e, 'priority')}
onTouchStart={(e) => startResize(e, 'priority')}
>
<div className="resize-handle-bar" />
</div>
</>
)}
{/* Kanban Board View */}