feat: view switcher improvements, header month option, kanban date/time, mobile fixes

- Swap simple/calendar icons in header view switcher
- Add calendar view button to header view switcher
- Reorder header: view switcher first, then days, hours, slot duration
- Add header display setting: calendar week (KW) or month name
- Show kanban stage color border on GridTaskBlock (week/simple views)
- Show date, weekday, and time on kanban cards
- Remove task swipe gestures on mobile (use container swipe for day nav)
- Fix mobile landscape 3-day grid overflow
- Add headerDisplay to schema, API, and settings UI

v1.30.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-11 20:22:32 +01:00
parent 8703239f0f
commit 329f082ecf
6 changed files with 139 additions and 102 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.29.0", "version": "1.30.0",
"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

@ -76,6 +76,7 @@ model User {
mobileDateLayout String @default("below") mobileDateLayout String @default("below")
dateAlignment String @default("center") dateAlignment String @default("center")
dateVerticalAlign String? @default("middle") dateVerticalAlign String? @default("middle")
headerDisplay String? @default("kw")
hourLabelFormat String @default("short") hourLabelFormat String @default("short")
showSubHourSlots Boolean @default(true) showSubHourSlots Boolean @default(true)
allDayPosition String @default("above") allDayPosition String @default("above")

View File

@ -45,6 +45,7 @@ export async function GET(request: NextRequest) {
dateLayout: true, dateLayout: true,
mobileDateLayout: true, mobileDateLayout: true,
dateVerticalAlign: true, dateVerticalAlign: true,
headerDisplay: true,
headlineFont: true, headlineFont: true,
headlineFontSize: true, headlineFontSize: true,
headlineFontWeight: true, headlineFontWeight: true,
@ -127,7 +128,7 @@ export async function PATCH(request: NextRequest) {
weekdayColor, dateColor, taskColor, todayHighlightColor, weekdayColor, dateColor, taskColor, todayHighlightColor,
pastDayColor, goalFallbackType, goalDefaultSentence, pastDayColor, goalFallbackType, goalDefaultSentence,
goalFontFamily, goalFontSize, goalFontWeight, goalScope, goalFontFamily, goalFontSize, goalFontWeight, goalScope,
dateLayout, mobileDateLayout, dateVerticalAlign, dateLayout, mobileDateLayout, dateVerticalAlign, headerDisplay,
hourLabelFormat, showSubHourSlots, allDayPosition, hourLabelFormat, showSubHourSlots, allDayPosition,
cwFontFamily, cwFontSize, cwFontWeight, cwColor, cwFontFamily, cwFontSize, cwFontWeight, cwColor,
yearFontFamily, yearFontSize, yearFontWeight, yearColor, yearFontFamily, yearFontSize, yearFontWeight, yearColor,
@ -192,6 +193,7 @@ export async function PATCH(request: NextRequest) {
...(dateLayout !== undefined && { dateLayout }), ...(dateLayout !== undefined && { dateLayout }),
...(mobileDateLayout !== undefined && { mobileDateLayout }), ...(mobileDateLayout !== undefined && { mobileDateLayout }),
...(dateVerticalAlign !== undefined && { dateVerticalAlign }), ...(dateVerticalAlign !== undefined && { dateVerticalAlign }),
...(headerDisplay !== undefined && { headerDisplay }),
...(hourLabelFormat !== undefined && { hourLabelFormat }), ...(hourLabelFormat !== undefined && { hourLabelFormat }),
...(showSubHourSlots !== undefined && { showSubHourSlots }), ...(showSubHourSlots !== undefined && { showSubHourSlots }),
...(allDayPosition !== undefined && { allDayPosition }), ...(allDayPosition !== undefined && { allDayPosition }),
@ -282,6 +284,7 @@ export async function PATCH(request: NextRequest) {
dateLayout: true, dateLayout: true,
mobileDateLayout: true, mobileDateLayout: true,
dateVerticalAlign: true, dateVerticalAlign: true,
headerDisplay: true,
cwFontFamily: true, cwFontFamily: true,
cwFontSize: true, cwFontSize: true,
cwFontWeight: true, cwFontWeight: true,

View File

@ -1855,6 +1855,11 @@ h3 {
padding-left: 20px; padding-left: 20px;
} }
.kanban-card-time {
font-weight: 600;
color: var(--weekly-text, #555);
}
.kanban-card-project { .kanban-card-project {
font-size: 0.7rem; font-size: 0.7rem;
margin-top: 2px; margin-top: 2px;
@ -2233,6 +2238,16 @@ h3 {
.side-nav { .side-nav {
display: none; display: none;
} }
/* Ensure grid respects viewDays columns on mobile landscape */
.time-grid-wrapper {
overflow: visible;
min-width: 0;
}
.time-grid-wrapper .weekly-days-grid {
overflow: visible;
min-width: 0;
}
} }
/* === Phone portrait (≤ 480px): 1-day view === */ /* === Phone portrait (≤ 480px): 1-day view === */

View File

@ -3,7 +3,7 @@ import { Repeat, Circle, X } from "lucide-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons"; import { faGoogle, faMicrosoft, faApple } from "@fortawesome/free-brands-svg-icons";
import { faServer } from "@fortawesome/free-solid-svg-icons"; import { faServer } from "@fortawesome/free-solid-svg-icons";
import { Task } from "./WeeklyView"; import { Task, KanbanStage } from "./WeeklyView";
interface GridTaskBlockProps { interface GridTaskBlockProps {
task: Task; task: Task;
@ -35,6 +35,7 @@ interface GridTaskBlockProps {
showTaskCheckboxes?: boolean; showTaskCheckboxes?: boolean;
projects?: any[]; projects?: any[];
onProjectAssign?: (taskId: string, projectId: string | null) => void; onProjectAssign?: (taskId: string, projectId: string | null) => void;
kanbanStages?: KanbanStage[];
} }
export function GridTaskBlock({ export function GridTaskBlock({
@ -66,7 +67,8 @@ export function GridTaskBlock({
workingHoursStart, workingHoursStart,
showTaskCheckboxes, showTaskCheckboxes,
projects, projects,
onProjectAssign onProjectAssign,
kanbanStages = []
}: GridTaskBlockProps) { }: GridTaskBlockProps) {
const [isNotesOpen, setIsNotesOpen] = useState(false); const [isNotesOpen, setIsNotesOpen] = useState(false);
const [notesValue, setNotesValue] = useState(task.markdownContent || ""); const [notesValue, setNotesValue] = useState(task.markdownContent || "");
@ -198,9 +200,14 @@ export function GridTaskBlock({
zIndex: isResizing || isNotesOpen || isSubTasksOpen ? 10 : 5, zIndex: isResizing || isNotesOpen || isSubTasksOpen ? 10 : 5,
background: (isNotesOpen || isSubTasksOpen || isResizing) ? (darkMode ? "#2a2a2a" : "#ffffff") : "transparent", background: (isNotesOpen || isSubTasksOpen || isResizing) ? (darkMode ? "#2a2a2a" : "#ffffff") : "transparent",
border: (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none", border: (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none",
borderLeft: task.project?.color ? `4px solid ${task.project.color}` : (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none", borderLeft: (() => {
const stageColor = task.kanbanStage ? kanbanStages.find(s => s.id === task.kanbanStage)?.color : null;
if (stageColor) return `4px solid ${stageColor}`;
if (task.project?.color) return `4px solid ${task.project.color}`;
return (isNotesOpen || isSubTasksOpen || isResizing) ? `1px solid ${darkMode ? "#404040" : "#e0e0e0"}` : "none";
})(),
borderRadius: (isNotesOpen || isSubTasksOpen || isResizing) ? "4px" : "0", borderRadius: (isNotesOpen || isSubTasksOpen || isResizing) ? "4px" : "0",
padding: task.project?.color ? "2px 4px 2px 8px" : "2px 4px", padding: (task.kanbanStage && kanbanStages.some(s => s.id === task.kanbanStage)) || task.project?.color ? "2px 4px 2px 8px" : "2px 4px",
boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none", boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",

View File

@ -258,6 +258,9 @@ const translations: Record<string, any> = {
addStage: "Add stage", addStage: "Add stage",
stageName: "Stage name", stageName: "Stage name",
noStage: "No stage", noStage: "No stage",
headerDisplay: "Header Display",
headerDisplayKW: "Calendar Week (KW)",
headerDisplayMonth: "Month Name",
language: "Language", language: "Language",
dateFormat: "Date Format", dateFormat: "Date Format",
timeFormat: "Time Format", timeFormat: "Time Format",
@ -461,6 +464,9 @@ const translations: Record<string, any> = {
addStage: "Phase hinzufügen", addStage: "Phase hinzufügen",
stageName: "Phasenname", stageName: "Phasenname",
noStage: "Keine Phase", noStage: "Keine Phase",
headerDisplay: "Kopfzeile",
headerDisplayKW: "Kalenderwoche (KW)",
headerDisplayMonth: "Monatsname",
listView: "Liste", listView: "Liste",
notes: "Notizen", notes: "Notizen",
notesSidebar: "Notizen-Seitenleiste", notesSidebar: "Notizen-Seitenleiste",
@ -667,6 +673,9 @@ const translations: Record<string, any> = {
addStage: "Ajouter une étape", addStage: "Ajouter une étape",
stageName: "Nom de l'étape", stageName: "Nom de l'étape",
noStage: "Aucune étape", noStage: "Aucune étape",
headerDisplay: "Affichage en-tête",
headerDisplayKW: "Semaine calendaire (KW)",
headerDisplayMonth: "Nom du mois",
language: "Langue", language: "Langue",
dateFormat: "Format de date", dateFormat: "Format de date",
timeFormat: "Format d'heure", timeFormat: "Format d'heure",
@ -871,6 +880,9 @@ const translations: Record<string, any> = {
addStage: "Añadir etapa", addStage: "Añadir etapa",
stageName: "Nombre de etapa", stageName: "Nombre de etapa",
noStage: "Sin etapa", noStage: "Sin etapa",
headerDisplay: "Visualización de encabezado",
headerDisplayKW: "Semana calendario (KW)",
headerDisplayMonth: "Nombre del mes",
language: "Idioma", language: "Idioma",
dateFormat: "Formato de fecha", dateFormat: "Formato de fecha",
timeFormat: "Formato de hora", timeFormat: "Formato de hora",
@ -1075,6 +1087,9 @@ const translations: Record<string, any> = {
addStage: "Aggiungi fase", addStage: "Aggiungi fase",
stageName: "Nome fase", stageName: "Nome fase",
noStage: "Nessuna fase", noStage: "Nessuna fase",
headerDisplay: "Visualizzazione intestazione",
headerDisplayKW: "Settimana calendario (KW)",
headerDisplayMonth: "Nome del mese",
language: "Lingua", language: "Lingua",
dateFormat: "Formato data", dateFormat: "Formato data",
timeFormat: "Formato ora", timeFormat: "Formato ora",
@ -1756,6 +1771,7 @@ export default function WeeklyView() {
weekdayCase?: "normal" | "capitalize" | "uppercase"; weekdayCase?: "normal" | "capitalize" | "uppercase";
customWeekdayNames?: string; customWeekdayNames?: string;
dateVerticalAlign?: "top" | "middle" | "bottom"; dateVerticalAlign?: "top" | "middle" | "bottom";
headerDisplay?: "kw" | "month";
}>({ }>({
name: session?.user?.name || "", name: session?.user?.name || "",
email: session?.user?.email || "", email: session?.user?.email || "",
@ -1903,7 +1919,7 @@ export default function WeeklyView() {
const [isSearchOpen, setIsSearchOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false);
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false); const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false); const [showDatePicker, setShowDatePicker] = useState(false);
const datePickerBtnRef = useRef<HTMLButtonElement>(null); const datePickerBtnRef = useRef<HTMLDivElement>(null);
const [showQuickSettings, setShowQuickSettings] = useState(false); const [showQuickSettings, setShowQuickSettings] = useState(false);
const [focusTimerDuration, setFocusTimerDuration] = useState(25); const [focusTimerDuration, setFocusTimerDuration] = useState(25);
@ -5454,29 +5470,39 @@ export default function WeeklyView() {
{/* Desktop Header: Left, Center, Right */} {/* Desktop Header: Left, Center, Right */}
<header className="group relative flex items-center justify-between w-full px-4 py-2 border-b border-gray-200 bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : {}}> <header className="group relative flex items-center justify-between w-full px-4 py-2 border-b border-gray-200 bg-white dark:bg-gray-900 dark:border-gray-700 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : {}}>
{/* LEFT SECTION: Slot Duration & Days to Show */} {/* LEFT SECTION: View Switcher, Days, Hours, Slot Duration */}
<div className="weekly-header-controls flex items-center gap-4 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 1 }}> <div className="weekly-header-controls flex items-center gap-4 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 1 }}>
{/* Slot Duration */} {/* View Switcher */}
{showTimeGrid && ( <div className="flex items-center gap-0.5 bg-gray-100 dark:bg-gray-800 rounded p-0.5" title={t.viewStyle}>
<div <button
className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1" onClick={() => { setViewStyle("simple"); setShowTimeGrid(true); saveSetting("viewStyle", "simple"); saveSetting("showTimeGrid", true); }}
title="Slot Duration" className={`p-1.5 rounded transition-colors ${viewStyle === "simple" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
title={t.simpleView}
> >
<Clock size={16} className="text-gray-500 mr-1" /> <Calendar size={16} />
{[15, 30, 60].map((duration) => ( </button>
<button <button
key={duration} onClick={() => { setViewStyle("calendar"); setShowTimeGrid(true); saveSetting("viewStyle", "calendar"); saveSetting("showTimeGrid", true); }}
onClick={() => { className={`p-1.5 rounded transition-colors ${viewStyle === "calendar" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
setCellDuration(duration as CellDuration); title={t.calendarView}
saveSetting("cellDuration", duration); >
}} <CalendarDays size={16} />
className={`px-2 py-0.5 text-xs rounded transition-colors ${cellDuration === duration ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`} </button>
> <button
{duration}m onClick={() => { setViewStyle("list"); setShowTimeGrid(false); saveSetting("viewStyle", "list"); saveSetting("showTimeGrid", false); }}
</button> className={`p-1.5 rounded transition-colors ${viewStyle === "list" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
))} title={t.listView}
</div> >
)} <ListTodo size={16} />
</button>
<button
onClick={() => { setViewStyle("kanban"); saveSetting("viewStyle", "kanban"); }}
className={`p-1.5 rounded transition-colors ${viewStyle === "kanban" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
title={t.kanbanView}
>
<Kanban size={16} />
</button>
</div>
{/* Days to Show */} {/* Days to Show */}
<div <div
@ -5542,30 +5568,27 @@ export default function WeeklyView() {
</div> </div>
)} )}
{/* View Switcher */} {/* Slot Duration */}
<div className="flex items-center gap-0.5 bg-gray-100 dark:bg-gray-800 rounded p-0.5" title={t.viewStyle}> {showTimeGrid && (
<button <div
onClick={() => { setViewStyle("simple"); saveSetting("viewStyle", "simple"); }} className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1"
className={`p-1.5 rounded transition-colors ${viewStyle !== "kanban" && viewStyle !== "list" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`} title="Slot Duration"
title={t.weekView}
> >
<CalendarDays size={16} /> <Clock size={16} className="text-gray-500 mr-1" />
</button> {[15, 30, 60].map((duration) => (
<button <button
onClick={() => { setViewStyle("list"); saveSetting("viewStyle", "list"); }} key={duration}
className={`p-1.5 rounded transition-colors ${viewStyle === "list" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`} onClick={() => {
title={t.listView} setCellDuration(duration as CellDuration);
> saveSetting("cellDuration", duration);
<ListTodo size={16} /> }}
</button> className={`px-2 py-0.5 text-xs rounded transition-colors ${cellDuration === duration ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
<button >
onClick={() => { setViewStyle("kanban"); saveSetting("viewStyle", "kanban"); }} {duration}m
className={`p-1.5 rounded transition-colors ${viewStyle === "kanban" ? "bg-white shadow-sm text-black dark:bg-gray-700 dark:text-white" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`} </button>
title={t.kanbanView} ))}
> </div>
<Kanban size={16} /> )}
</button>
</div>
</div> </div>
@ -5573,16 +5596,13 @@ export default function WeeklyView() {
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group z-10 opacity-100" style={{ pointerEvents: "auto" }}> <div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group z-10 opacity-100" style={{ pointerEvents: "auto" }}>
{/* Week & Year */} {/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2"> <div className="whitespace-nowrap flex items-center gap-2">
{/* Date Picker Toggle - Moved to front */} {/* Clickable Week & Year */}
<div className="relative"> <div
<button ref={datePickerBtnRef}
ref={datePickerBtnRef} className="relative flex items-center gap-2 cursor-pointer hover:opacity-80"
className={`p-1.5 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors ${showDatePicker ? "text-teal-600 bg-teal-50 opacity-100" : "text-gray-500 hover:text-black opacity-0 group-hover:opacity-100"}`} onClick={() => setShowDatePicker(!showDatePicker)}
onClick={() => setShowDatePicker(!showDatePicker)} title="Jump to date"
title="Jump to date" >
>
<Calendar size={18} />
</button>
{showDatePicker && ( {showDatePicker && (
<SimpleDatePicker <SimpleDatePicker
selected={currentWeekStart} selected={currentWeekStart}
@ -5595,14 +5615,6 @@ export default function WeeklyView() {
anchorRef={datePickerBtnRef} anchorRef={datePickerBtnRef}
/> />
)} )}
</div>
{/* Clickable Week & Year */}
<div
className="flex items-center gap-2 cursor-pointer hover:opacity-80"
onClick={() => setShowDatePicker(!showDatePicker)}
title="Jump to date"
>
<span style={{ <span style={{
fontFamily: profile.cwFontFamily || "Inter", fontFamily: profile.cwFontFamily || "Inter",
fontSize: profile.cwFontSize || "1.125rem", fontSize: profile.cwFontSize || "1.125rem",
@ -5610,7 +5622,10 @@ export default function WeeklyView() {
color: adjustColorForDarkMode(profile.cwColor || "#333333", darkMode), color: adjustColorForDarkMode(profile.cwColor || "#333333", darkMode),
filter: "brightness(var(--weekly-header-brightness, 1))" filter: "brightness(var(--weekly-header-brightness, 1))"
}}> }}>
KW {getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} {profile.headerDisplay === "month"
? getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: "long" })
: `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")}`
}
</span> </span>
<span className="text-gray-400">|</span> <span className="text-gray-400">|</span>
<span <span
@ -5962,9 +5977,10 @@ export default function WeeklyView() {
{task.title} {task.title}
</span> </span>
</div> </div>
{task.scheduledDate && ( {(task.scheduledDate || task.startTime) && (
<div className="kanban-card-date"> <div className="kanban-card-date">
{new Date(task.scheduledDate).toLocaleDateString(language, { month: "short", day: "numeric" })} {task.scheduledDate && new Date(task.scheduledDate).toLocaleDateString(language, { weekday: "short", month: "short", day: "numeric" })}
{task.startTime && <span className="kanban-card-time"> {task.startTime}</span>}
</div> </div>
)} )}
{task.project && ( {task.project && (
@ -6410,6 +6426,7 @@ export default function WeeklyView() {
showTaskCheckboxes={profile.showTaskCheckboxes} showTaskCheckboxes={profile.showTaskCheckboxes}
projects={projects} projects={projects}
onProjectAssign={assignProject} onProjectAssign={assignProject}
kanbanStages={kanbanStages}
/> />
))} ))}
{visibleSlots.map((slot) => { {visibleSlots.map((slot) => {
@ -8365,36 +8382,8 @@ function TaskItem({
onEdit(); onEdit();
} }
}} }}
onTouchStart={(e) => { onTouchStart={() => {
const touch = e.touches[0]; // No task-level swipe — container handles day navigation
swipeTouchStart.current = { x: touch.clientX, y: touch.clientY, swiping: false };
setSwipeX(0);
}}
onTouchMove={(e) => {
const touch = e.touches[0];
const dx = touch.clientX - swipeTouchStart.current.x;
const dy = touch.clientY - swipeTouchStart.current.y;
// Only swipe if horizontal dominant and past 10px threshold
if (!swipeTouchStart.current.swiping && Math.abs(dx) > 10 && Math.abs(dx) > Math.abs(dy) * 1.5) {
swipeTouchStart.current.swiping = true;
}
if (swipeTouchStart.current.swiping) {
e.preventDefault();
setSwipeX(dx);
}
}}
onTouchEnd={() => {
if (Math.abs(swipeX) > 80) {
if (swipeX > 0) {
// Swipe right: toggle complete
onToggle();
} else {
// Swipe left: delete
onDelete();
}
}
setSwipeX(0);
swipeTouchStart.current.swiping = false;
}} }}
> >
{/* Swipe indicators */} {/* Swipe indicators */}
@ -9565,6 +9554,7 @@ function SettingsSidebar({
weekdayCase?: "normal" | "capitalize" | "uppercase"; weekdayCase?: "normal" | "capitalize" | "uppercase";
customWeekdayNames?: string; customWeekdayNames?: string;
dateVerticalAlign?: "top" | "middle" | "bottom"; dateVerticalAlign?: "top" | "middle" | "bottom";
headerDisplay?: "kw" | "month";
}>({ }>({
name: "", name: "",
email: "", email: "",
@ -10680,6 +10670,27 @@ function SettingsSidebar({
</div> </div>
</div> </div>
{/* Header Display */}
<div style={{ marginTop: "16px" }}>
<label style={{ fontSize: "0.85rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{t.headerDisplay}
</label>
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded p-1 mt-1" style={{ width: "fit-content" }}>
<button
onClick={() => setProfile({ ...profile, headerDisplay: "kw" })}
className={`px-4 py-2 text-sm rounded transition-colors ${(profile.headerDisplay || "kw") === "kw" ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
>
{t.headerDisplayKW}
</button>
<button
onClick={() => setProfile({ ...profile, headerDisplay: "month" })}
className={`px-4 py-2 text-sm rounded transition-colors ${profile.headerDisplay === "month" ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`}
>
{t.headerDisplayMonth}
</button>
</div>
</div>
{/* Kanban Stages Settings */} {/* Kanban Stages Settings */}
<div style={{ marginTop: "24px", borderTop: "1px solid var(--border-color, #e5e7eb)", paddingTop: "16px" }}> <div style={{ marginTop: "24px", borderTop: "1px solid var(--border-color, #e5e7eb)", paddingTop: "16px" }}>
<h4 style={{ fontSize: "0.95rem", fontWeight: 700, marginBottom: "8px", display: "flex", alignItems: "center", gap: "6px" }}> <h4 style={{ fontSize: "0.95rem", fontWeight: 700, marginBottom: "8px", display: "flex", alignItems: "center", gap: "6px" }}>