From 224fdba5c69221ac3e09e48480c1dcac7a865c8b Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Thu, 3 Sep 2026 10:14:50 +0200 Subject: [PATCH] feat: custom day count for the week view Add a "Custom" option next to the 1/2/3/5/7 presets where any number of days from 1 to 31 can be typed. The seven duplicated preset button rows are replaced by a shared DaysSelector component with panel/header/menu variants. The day grid and all-day grid only had CSS for cols-1/2/3/5/7, so any other count silently fell back to five columns; both now derive grid-template-columns from the actual day count. v1.115.0 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 13 +++ package.json | 2 +- src/components/DaysSelector.tsx | 183 ++++++++++++++++++++++++++++++++ src/components/WeeklyView.tsx | 46 ++++---- 4 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 src/components/DaysSelector.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e546372..51995c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project are documented in this file. +## [1.115.0] - 2026-09-03 + +### Added +- Day columns: a "Custom" option next to the 1/2/3/5/7 presets, where any number of days from + 1 to 31 can be typed (e.g. 14, 21, 28). The chosen value shows up as its own chip beside the + presets and is available in every place the day count can be set — sidebar, both settings + flyouts, the desktop toolbar and the header overflow menu. + +### Fixed +- Day grid and all-day grid only had CSS for `cols-1/2/3/5/7`, so any other day count silently + fell back to a 5-column layout. Both grids now derive `grid-template-columns` from the actual + day count. + ## [1.114.2] - 2026-09-01 ### Fixed diff --git a/package.json b/package.json index 720ee5c..05b21b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.114.2", + "version": "1.115.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": { diff --git a/src/components/DaysSelector.tsx b/src/components/DaysSelector.tsx new file mode 100644 index 0000000..678df21 --- /dev/null +++ b/src/components/DaysSelector.tsx @@ -0,0 +1,183 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; + +export const DAY_PRESETS = [1, 2, 3, 5, 7]; +export const MAX_VIEW_DAYS = 31; + +export function clampViewDays(n: number): number { + if (!Number.isFinite(n)) return 7; + return Math.min(MAX_VIEW_DAYS, Math.max(1, Math.round(n))); +} + +type Variant = "panel" | "header" | "menu"; + +interface DaysSelectorProps { + viewDays: number; + onChange: (days: number) => void; + darkMode?: boolean; + language?: string; + variant?: Variant; +} + +/** + * Day-column count picker: the fixed presets plus a "Custom" field where the + * user types any number of days (1–31), e.g. 14, 21, 28. + */ +export default function DaysSelector({ + viewDays, + onChange, + darkMode = false, + language = "en", + variant = "panel", +}: DaysSelectorProps) { + const isCustom = !DAY_PRESETS.includes(viewDays); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(String(viewDays)); + const inputRef = useRef(null); + + useEffect(() => { + if (editing) { + setDraft(String(viewDays)); + inputRef.current?.focus(); + inputRef.current?.select(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editing]); + + const commit = () => { + const parsed = parseInt(draft, 10); + if (!isNaN(parsed)) onChange(clampViewDays(parsed)); + setEditing(false); + }; + + const isDE = language === "de"; + const customLabel = isDE ? "Eigene Anzahl Tage" : "Custom number of days"; + + const buttons = isCustom ? [...DAY_PRESETS, viewDays] : DAY_PRESETS; + + if (variant === "header") { + const cls = (active: boolean) => + `px-2 py-0.5 text-xs rounded transition-colors ${active ? "bg-white shadow-sm font-bold text-black" : "text-gray-500 hover:text-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700"}`; + return ( + <> + {buttons.map((num) => ( + + ))} + {editing ? ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} + className="w-12 px-1 py-0.5 text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-black dark:text-white" + /> + ) : ( + + )} + + ); + } + + if (variant === "menu") { + const btnStyle = (active: boolean): React.CSSProperties => ({ + padding: "2px 8px", + fontSize: "12px", + borderRadius: "4px", + border: "none", + cursor: "pointer", + fontWeight: active ? 700 : 400, + background: active ? "var(--bg-secondary, #e5e7eb)" : "transparent", + color: "inherit", + }); + return ( + <> + {buttons.map((num) => ( + + ))} + {editing ? ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} + style={{ width: "48px", padding: "2px 4px", fontSize: "12px", borderRadius: "4px", border: "1px solid var(--line, #d1d5db)", background: "transparent", color: "inherit" }} + /> + ) : ( + + )} + + ); + } + + // "panel" — sidebar / flyout chip row + const chipStyle = (active: boolean): React.CSSProperties => ({ + flex: 1, + padding: "5px 0", + fontSize: "0.8rem", + borderRadius: "6px", + border: "none", + cursor: "pointer", + fontWeight: active ? 700 : 400, + background: active ? (darkMode ? "#374151" : "var(--ink)") : (darkMode ? "#1f2937" : "#e5e7eb"), + color: active ? "#fff" : (darkMode ? "#9ca3af" : "var(--ink-3)"), + }); + + return ( +
+
+ {buttons.map((num) => ( + + ))} + +
+ {editing && ( +
+ setDraft(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} + placeholder={`1-${MAX_VIEW_DAYS}`} + style={{ + flex: 1, + minWidth: 0, + padding: "5px 6px", + fontSize: "0.8rem", + borderRadius: "6px", + border: `1px solid ${darkMode ? "#374151" : "var(--line, #d1d5db)"}`, + background: darkMode ? "#111827" : "#fff", + color: darkMode ? "#e5e7eb" : "var(--ink)", + }} + /> + +
+ )} +
+ ); +} diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 40298a7..29cd18a 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -31,6 +31,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import FocusModeOverlay from "./FocusModeOverlay"; +import DaysSelector, { clampViewDays } from "./DaysSelector"; import { LayoutGrid, LayoutPanelLeft, @@ -2455,6 +2456,13 @@ export default function WeeklyView() { } }; + const handleViewDaysChange = (num: number) => { + const days = clampViewDays(num); + setViewDays(days); + savedViewDaysRef.current = days; + saveSetting("viewDays", days); + }; + const handleSettingsChanged = (newSettings: any) => { setShowTimeGrid(newSettings.showTimeGrid); setCellDuration(newSettings.cellDuration); @@ -5645,7 +5653,7 @@ export default function WeeklyView() { {isAllDayExpanded && (
{getVisibleDays().map((date) => { const dayEvents = getAllDayEventsForDate(date); @@ -5788,7 +5796,7 @@ export default function WeeklyView() {
{profile.language === "de" ? "Spalten" : "Days"} -
{[1, 2, 3, 5, 7].map((num) => ())}
+
{profile.showTimeGrid && (
{profile.language === "de" ? "Zeitfenster" : "Slot"}
{(([15, 20, 30, 60] as CellDuration[])).map((d) => ())}
)} {profile.showTimeGrid && (
:15/:30/:45
)} @@ -5847,7 +5855,7 @@ export default function WeeklyView() {
{profile.language === "de" ? "Ansicht" : "View"}
{[{ key: "simple", icon: }, { key: "calendar", icon: }, { key: "list", icon: }, { key: "kanban", icon: }, { key: "priority", icon: }].map((v) => ())}
-
{profile.language === "de" ? "Spalten" : "Days"}
{[1, 2, 3, 5, 7].map((num) => ())}
+
{profile.language === "de" ? "Spalten" : "Days"}
{profile.showTimeGrid && (
{profile.language === "de" ? "Zeitfenster" : "Slot"}
{(([15, 20, 30, 60] as CellDuration[])).map((d) => ())}
)} {profile.showTimeGrid && (
:15/:30/:45
)}
{profile.language === "de" ? "Textgröße" : "Text size"}
{(["S", "M", "L"] as const).map((size) => ())}
@@ -5919,7 +5927,7 @@ export default function WeeklyView() { {/* Columns / Days */}
{profile.language === "de" ? "Spalten" : "Days"} -
{[1, 2, 3, 5, 7].map((num) => ())}
+
{/* Slot Duration */} {profile.showTimeGrid && (
{profile.language === "de" ? "Zeitfenster" : "Slot"}
{(([15, 20, 30, 60] as CellDuration[])).map((d) => ())}
)} @@ -6046,7 +6054,7 @@ export default function WeeklyView() { {flyoutSection === "columns" && ( <>
{profile.language === "de" ? "Spalten" : "Days"}
-
{[1, 2, 3, 5, 7].map((num) => ())}
+ )} {flyoutSection === "slot" && ( @@ -6259,7 +6267,7 @@ export default function WeeklyView() { {flyoutSection === "columns" && ( <>
{profile.language === "de" ? "Spalten" : "Days"}
-
{[1, 2, 3, 5, 7].map((num) => ())}
+ )} {flyoutSection === "slot" && ( @@ -6344,19 +6352,7 @@ export default function WeeklyView() { title="Days to show" > - {[1, 2, 3, 5, 7].map((num) => ( - - ))} +
{/* Slot Duration — desktop only (available in sidebar on tablet) */} @@ -6634,15 +6630,7 @@ export default function WeeklyView() {
{profile.language === "de" ? "Tage" : "Days"}: - {[1, 2, 3, 5, 7].map((num) => ( - - ))} +
{profile.showTimeGrid && (
@@ -7404,10 +7392,12 @@ export default function WeeklyView() { data-slide-direction={slideDirection} data-nav-type={viewDays > 1 ? "week" : "day"} style={profile.showTimeGrid ? { + gridTemplateColumns: `repeat(${viewDays}, minmax(0, 1fr))`, height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + getHeaderHeight(effectiveCellDuration)}px`, flex: 1, alignSelf: "flex-start", } : { + gridTemplateColumns: `repeat(${viewDays}, minmax(0, 1fr))`, flex: 1, }} >