fix: improved subtask expand/collapse in calendar and kanban views

Kanban cards now have inline subtask expand/collapse with progress bar,
chevron toggle, and checkbox list. Calendar view subtask badge updated
to match with ChevronDown icon, highlight when expanded, and wider
progress bar. Consistent UX across both views.

v1.55.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-22 08:34:43 +01:00
parent 22a193180b
commit 1f90526b5b
3 changed files with 124 additions and 18 deletions

View File

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

View File

@ -1952,6 +1952,73 @@ h3 {
} }
/* Kanban stage color indicator on weekly tasks */ /* Kanban stage color indicator on weekly tasks */
.kanban-card-subtask-toggle {
margin-top: 4px;
}
.kanban-card-subtask-btn {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: none;
cursor: pointer;
padding: 3px 0;
color: var(--weekly-text-muted, #9ca3af);
font-size: 0.7rem;
font-weight: 600;
width: 100%;
}
.kanban-card-subtask-btn:hover {
color: var(--weekly-text, #666);
}
.kanban-card-subtask-bar {
flex: 1;
height: 4px;
border-radius: 2px;
background: var(--weekly-border, #e5e7eb);
position: relative;
overflow: hidden;
max-width: 60px;
}
.kanban-card-subtask-bar-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
border-radius: 2px;
transition: width 0.3s ease;
}
.kanban-card-subtask-count {
font-size: 0.7rem;
opacity: 0.8;
}
.kanban-card-subtask-list {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 0 2px 2px;
}
.kanban-card-subtask-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--weekly-text, #333);
padding: 1px 0;
}
.kanban-card-subtask-item input[type="checkbox"] {
width: 13px;
height: 13px;
cursor: pointer;
flex-shrink: 0;
}
.kanban-card-subtask-item.completed {
opacity: 0.5;
}
.kanban-card-subtask-done {
text-decoration: line-through;
}
.kanban-stage-indicator { .kanban-stage-indicator {
width: 4px; width: 4px;
border-radius: 2px; border-radius: 2px;

View File

@ -2078,6 +2078,7 @@ export default function WeeklyView() {
const [kanbanAddingStageId, setKanbanAddingStageId] = useState<string | null>(null); const [kanbanAddingStageId, setKanbanAddingStageId] = useState<string | null>(null);
const [kanbanNewTaskTitle, setKanbanNewTaskTitle] = useState(""); const [kanbanNewTaskTitle, setKanbanNewTaskTitle] = useState("");
const [kanbanDetailTask, setKanbanDetailTask] = useState<Task | null>(null); const [kanbanDetailTask, setKanbanDetailTask] = useState<Task | null>(null);
const [kanbanExpandedCards, setKanbanExpandedCards] = useState<Set<string>>(new Set());
const [protectEventTimes, setProtectEventTimes] = useState(false); const [protectEventTimes, setProtectEventTimes] = useState(false);
const [unlockedEvents, setUnlockedEvents] = useState<Set<string>>(new Set()); const [unlockedEvents, setUnlockedEvents] = useState<Set<string>>(new Set());
@ -6447,7 +6448,15 @@ export default function WeeklyView() {
return true; return true;
}); });
const renderKanbanCard = (task: Task) => ( const renderKanbanCard = (task: Task) => {
const subTasks = task.subTasks || [];
const subCompleted = subTasks.filter(s => s.completed).length;
const subTotal = subTasks.length;
const subPct = subTotal > 0 ? (subCompleted / subTotal) * 100 : 0;
const allSubDone = subTotal > 0 && subCompleted === subTotal;
const isExpanded = kanbanExpandedCards.has(task.id);
return (
<div <div
key={task.id} key={task.id}
className={`kanban-card ${task.completed ? "kanban-card-done" : ""}`} className={`kanban-card ${task.completed ? "kanban-card-done" : ""}`}
@ -6457,9 +6466,8 @@ export default function WeeklyView() {
e.dataTransfer.effectAllowed = "move"; e.dataTransfer.effectAllowed = "move";
}} }}
onClick={(e) => { onClick={(e) => {
// Don't open detail if clicking checkbox, contentEditable, or buttons
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.tagName === "INPUT" || target.contentEditable === "true" || target.closest("button")) return; if (target.tagName === "INPUT" || target.contentEditable === "true" || target.closest("button") || target.closest(".kanban-card-subtask-list")) return;
setKanbanDetailTask(task); setKanbanDetailTask(task);
}} }}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
@ -6488,9 +6496,42 @@ export default function WeeklyView() {
{task.title} {task.title}
</span> </span>
</div> </div>
{task.subTasks && task.subTasks.length > 0 && ( {subTotal > 0 && (
<div className="kanban-card-subtasks" style={{ fontSize: "0.7rem", color: "#9ca3af", marginTop: "2px" }}> <div className="kanban-card-subtask-toggle" onClick={(e) => e.stopPropagation()}>
{task.subTasks.filter(st => st.completed).length}/{task.subTasks.length} {language === "de" ? "Unteraufgaben" : "subtasks"} <button
className="kanban-card-subtask-btn"
onClick={() => {
setKanbanExpandedCards(prev => {
const next = new Set(prev);
if (next.has(task.id)) next.delete(task.id);
else next.add(task.id);
return next;
});
}}
>
<ChevronDown size={12} style={{ transform: isExpanded ? "rotate(0deg)" : "rotate(-90deg)", transition: "transform 0.2s" }} />
<span className="kanban-card-subtask-bar">
<span className="kanban-card-subtask-bar-fill" style={{ width: `${subPct}%`, background: allSubDone ? "#0d9488" : "var(--weekly-accent, #6366f1)" }} />
</span>
<span className="kanban-card-subtask-count" style={{ color: allSubDone ? "#0d9488" : undefined }}>
{subCompleted}/{subTotal}
</span>
</button>
{isExpanded && (
<div className="kanban-card-subtask-list">
{subTasks.map(sub => (
<div key={sub.id} className={`kanban-card-subtask-item ${sub.completed ? "completed" : ""}`}>
<input
type="checkbox"
checked={sub.completed}
onChange={() => toggleSubTask(sub.id)}
style={{ accentColor: "var(--weekly-accent, #6366f1)" }}
/>
<span className={sub.completed ? "kanban-card-subtask-done" : ""}>{sub.title}</span>
</div>
))}
</div>
)}
</div> </div>
)} )}
{task.markdownContent && ( {task.markdownContent && (
@ -6515,6 +6556,7 @@ export default function WeeklyView() {
})()} })()}
</div> </div>
); );
};
const kanbanColumnDrop = (stageId: string | null) => ({ const kanbanColumnDrop = (stageId: string | null) => ({
onDragOver: (e: React.DragEvent) => { onDragOver: (e: React.DragEvent) => {
@ -9710,9 +9752,9 @@ function TaskItem({
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
gap: "4px", gap: "4px",
padding: "2px 8px 2px 6px", padding: "2px 8px 2px 4px",
borderRadius: "12px", borderRadius: "12px",
background: "transparent", background: expanded ? "rgba(99,102,241,0.08)" : "transparent",
color: allDone ? "var(--weekly-teal, #0d9488)" : "var(--weekly-text-light, #9ca3af)", color: allDone ? "var(--weekly-teal, #0d9488)" : "var(--weekly-text-light, #9ca3af)",
border: "none", border: "none",
cursor: "pointer", cursor: "pointer",
@ -9721,17 +9763,14 @@ function TaskItem({
lineHeight: 1, lineHeight: 1,
flexShrink: 0, flexShrink: 0,
whiteSpace: "nowrap", whiteSpace: "nowrap",
transition: "color 0.15s", transition: "all 0.15s",
}} }}
> >
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ transform: expanded ? "rotate(90deg)" : "rotate(0deg)", transition: "transform 0.2s" }}> <ChevronDown size={11} style={{ transform: expanded ? "rotate(0deg)" : "rotate(-90deg)", transition: "transform 0.2s" }} />
<polyline points="9 18 15 12 9 6" />
</svg>
{/* Mini progress bar */}
<span style={{ <span style={{
display: "inline-block", display: "inline-block",
width: "20px", width: "24px",
height: "3px", height: "4px",
borderRadius: "2px", borderRadius: "2px",
background: "var(--weekly-border, #e5e7eb)", background: "var(--weekly-border, #e5e7eb)",
position: "relative", position: "relative",
@ -9744,7 +9783,7 @@ function TaskItem({
height: "100%", height: "100%",
width: `${pct}%`, width: `${pct}%`,
borderRadius: "2px", borderRadius: "2px",
background: allDone ? "var(--weekly-teal, #0d9488)" : "#6366f1", background: allDone ? "var(--weekly-teal, #0d9488)" : "var(--weekly-accent, #6366f1)",
transition: "width 0.3s ease", transition: "width 0.3s ease",
}} /> }} />
</span> </span>