Refine sidebar header controls and resize handles

This commit is contained in:
mARTin-B78 2026-05-13 00:43:19 +02:00
parent 07401d3dfa
commit 5ebdf6d751
3 changed files with 161 additions and 49 deletions

View File

@ -3270,26 +3270,29 @@ h3 {
/* Resize handle for draggable section borders */ /* Resize handle for draggable section borders */
.resize-handle { .resize-handle {
height: 7px; height: 2px;
border: 1px solid #ccc;
cursor: ns-resize; cursor: ns-resize;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none; user-select: none;
touch-action: none; touch-action: none;
position: relative; position: relative;
z-index: 10; z-index: 30;
flex-shrink: 0; flex-shrink: 0;
border-top: 1px solid var(--weekly-border, #e5e7eb); overflow: visible;
border-bottom: 1px solid var(--weekly-border, #e5e7eb); background-color: #ddd;
} }
.resize-handle:hover { .resize-handle:hover {
background: rgba(59, 130, 246, 0.08); background-color: #d7d7d7;
border-color: var(--weekly-accent, #3b82f6); border-color: #b8b8b8;
} }
.resize-handle:active { .resize-handle:active {
background: rgba(59, 130, 246, 0.12); background-color: #d1d1d1;
border-color: var(--weekly-accent, #3b82f6); border-color: #aaa;
} }
.resize-handle-bar { .resize-handle-bar {
width: 32px; width: 32px;
@ -3299,10 +3302,50 @@ h3 {
opacity: 0.6; opacity: 0.6;
transition: background 0.15s, opacity 0.15s; transition: background 0.15s, opacity 0.15s;
} }
.resize-handle-anyday {
height: 2px;
}
.resize-handle-allday {
height: 2px;
}
.resize-handle-anyday .resize-handle-bar {
width: 23px;
height: 9px;
border-radius: 4px;
background: radial-gradient(circle, color-mix(in srgb, var(--weekly-text, #333) 48%, transparent) 1.25px, transparent 1.35px) 2px 1.5px / 6px 5px repeat;
background-color: #ddd;
border: 1px solid #ccc;
opacity: 1;
position: relative;
bottom: 0;
overflow: visible;
z-index: 50;
}
.resize-handle-allday .resize-handle-bar {
width: 23px;
height: 9px;
border-radius: 4px;
background: radial-gradient(circle, color-mix(in srgb, var(--weekly-text, #333) 48%, transparent) 1.25px, transparent 1.35px) 2px 1.5px / 6px 5px repeat;
background-color: #ddd;
border: 1px solid #ccc;
opacity: 1;
position: relative;
bottom: 0;
overflow: visible;
z-index: 50;
}
.resize-handle:hover .resize-handle-bar, .resize-handle:hover .resize-handle-bar,
.resize-handle:active .resize-handle-bar { .resize-handle:active .resize-handle-bar {
background: var(--weekly-accent, #3b82f6); opacity: 1;
opacity: 0.5; }
.resize-handle-anyday:hover .resize-handle-bar,
.resize-handle-anyday:active .resize-handle-bar,
.resize-handle-allday:hover .resize-handle-bar,
.resize-handle-allday:active .resize-handle-bar {
background: radial-gradient(circle, var(--weekly-accent, #3b82f6) 1.25px, transparent 1.35px) 2px 1.5px / 6px 5px repeat;
background-color: #d7d7d7;
border-color: #b8b8b8;
opacity: 1;
} }
.all-day-events-header { .all-day-events-header {

View File

@ -1045,6 +1045,41 @@ function SettingsSidebar({
</button> </button>
))} ))}
</div> </div>
<div style={{ marginTop: "10px" }}>
<label style={{ fontSize: "0.78rem", fontWeight: 600, color: "var(--weekly-settings-label)" }}>
{profile.language === "de" ? "Steuerung in der Kopfleiste" : "Header Controls"}
</label>
<div className="mt-1 flex gap-2">
{[
{ value: true, label: profile.language === "de" ? "Anzeigen" : "Show" },
{ value: false, label: profile.language === "de" ? "Ausblenden" : "Hide" },
].map(({ value, label }) => {
const isActive = (profile.showHeaderControls !== false) === value;
return (
<button
key={String(value)}
onClick={() => {
setProfile((p: any) => ({ ...p, showHeaderControls: value }));
saveSetting("showHeaderControls", value);
}}
style={{
flex: 1,
padding: "6px 10px",
fontSize: "0.8rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border, #e5e7eb)",
cursor: "pointer",
fontWeight: isActive ? 700 : 400,
background: isActive ? "var(--weekly-text, #333)" : "transparent",
color: isActive ? "#fff" : "var(--weekly-settings-label)",
}}
>
{label}
</button>
);
})}
</div>
</div>
</div> </div>
{/* Header Display */} {/* Header Display */}
<div> <div>

View File

@ -128,6 +128,7 @@ type DeviceViewSettingKey = typeof DEVICE_VIEW_SETTINGS_KEYS[number];
// Settings that save to DB (cross-device default) AND to cookie (device override wins on load) // Settings that save to DB (cross-device default) AND to cookie (device override wins on load)
const DEVICE_ALSO_COOKIE_KEYS = ["viewStyle", "showTimeGrid", "startDayOffset"]; const DEVICE_ALSO_COOKIE_KEYS = ["viewStyle", "showTimeGrid", "startDayOffset"];
const VIEW_SETTINGS_PROFILE_KEYS = ["menuPosition", "showHeaderControls"];
function getCookie(name: string): string | null { function getCookie(name: string): string | null {
if (typeof document === "undefined") return null; if (typeof document === "undefined") return null;
@ -918,6 +919,7 @@ export default function WeeklyView() {
// Mobile detection // Mobile detection
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const [isPortrait, setIsPortrait] = useState(false); const [isPortrait, setIsPortrait] = useState(false);
const [isCompactHeight, setIsCompactHeight] = useState(false);
// Tracks the last day column the user interacted with (for "selected day" header display) // Tracks the last day column the user interacted with (for "selected day" header display)
const [selectedDay, setSelectedDay] = useState<Date | null>(null); const [selectedDay, setSelectedDay] = useState<Date | null>(null);
@ -1037,6 +1039,8 @@ export default function WeeklyView() {
weatherLon: null, weatherLon: null,
weatherLocation: "", weatherLocation: "",
showCalendarProviderIcon: false, showCalendarProviderIcon: false,
menuPosition: "left",
showHeaderControls: true,
}); });
const [motivationalQuote, setMotivationalQuote] = useState(""); const [motivationalQuote, setMotivationalQuote] = useState("");
const [showSummary, setShowSummary] = useState(false); const [showSummary, setShowSummary] = useState(false);
@ -1400,6 +1404,7 @@ export default function WeeklyView() {
const check = () => { const check = () => {
setIsMobile(window.innerWidth <= 768); setIsMobile(window.innerWidth <= 768);
setIsPortrait(window.innerHeight > window.innerWidth); setIsPortrait(window.innerHeight > window.innerWidth);
setIsCompactHeight(window.innerHeight <= 720);
}; };
check(); check();
window.addEventListener("resize", check); window.addEventListener("resize", check);
@ -1416,7 +1421,12 @@ export default function WeeklyView() {
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
if (data && data.user) { if (data && data.user) {
const profileData = data.user; const viewPrefs = (data.user.viewSettings || {}) as any;
const profileData = {
...data.user,
menuPosition: viewPrefs.menuPosition ?? data.user.menuPosition ?? "left",
showHeaderControls: viewPrefs.showHeaderControls ?? data.user.showHeaderControls ?? true,
};
setProfile(profileData); setProfile(profileData);
// Sync individual states to profile data // Sync individual states to profile data
@ -2403,6 +2413,22 @@ export default function WeeklyView() {
somedayWheelCleanup.current = () => node.removeEventListener("wheel", handler); somedayWheelCleanup.current = () => node.removeEventListener("wheel", handler);
}, []); }, []);
const saveSetting = async (key: string, value: any) => { const saveSetting = async (key: string, value: any) => {
if (VIEW_SETTINGS_PROFILE_KEYS.includes(key)) {
const updated = { ...(viewSettingsRef.current as any), [key]: value };
viewSettingsRef.current = updated;
setViewSettings(updated as any);
setProfile((p: any) => ({ ...p, [key]: value }));
try {
await fetch("/api/user/profile", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ viewSettings: updated }),
});
} catch (err) {
console.error(`Failed to save setting ${key}:`, err);
}
return;
}
// Per-device settings: save to cookie ONLY (not DB) so each device keeps its own value // Per-device settings: save to cookie ONLY (not DB) so each device keeps its own value
if (DEVICE_SETTINGS_KEYS.includes(key)) { if (DEVICE_SETTINGS_KEYS.includes(key)) {
setCookie(`setting_${key}`, String(value)); setCookie(`setting_${key}`, String(value));
@ -5383,6 +5409,10 @@ export default function WeeklyView() {
}; };
const activeTheme = (darkMode ? profile.darkTheme : profile.lightTheme) as Record<string, string> | null; const activeTheme = (darkMode ? profile.darkTheme : profile.lightTheme) as Record<string, string> | null;
const useLeftRail = !isMobile && profile.menuPosition !== "top"; const useLeftRail = !isMobile && profile.menuPosition !== "top";
const showHeaderControls = profile.showHeaderControls !== false;
const compactRail = useLeftRail && isCompactHeight;
const collapsedRailWidth = compactRail ? 38 : 44;
const expandedRailWidth = compactRail ? 220 : 240;
const containerStyle = { const containerStyle = {
...(activeTheme ? { ...(activeTheme ? {
@ -5461,7 +5491,7 @@ export default function WeeklyView() {
"--weekly-past-color": activeTheme?.color8 || (darkMode "--weekly-past-color": activeTheme?.color8 || (darkMode
? invertColor(profile.pastDayColor || "#a6a6a7") ? invertColor(profile.pastDayColor || "#a6a6a7")
: profile.pastDayColor || "#a6a6a7"), : profile.pastDayColor || "#a6a6a7"),
...(useLeftRail ? { paddingLeft: leftRailExpanded ? "240px" : "44px", transition: "padding-left 0.2s ease" } : {}), ...(useLeftRail ? { paddingLeft: leftRailExpanded ? `${expandedRailWidth}px` : `${collapsedRailWidth}px`, transition: "padding-left 0.2s ease" } : {}),
} as React.CSSProperties; } as React.CSSProperties;
if (isLoading) { if (isLoading) {
@ -5492,12 +5522,12 @@ export default function WeeklyView() {
}); });
const railIconBtnStyle: React.CSSProperties = { const railIconBtnStyle: React.CSSProperties = {
width: "36px", height: "36px", display: "flex", alignItems: "center", justifyContent: "center", width: compactRail ? "30px" : "36px", height: compactRail ? "30px" : "36px", display: "flex", alignItems: "center", justifyContent: "center",
background: "none", border: "none", cursor: "pointer", borderRadius: "8px", background: "none", border: "none", cursor: "pointer", borderRadius: compactRail ? "7px" : "8px",
color: darkMode ? "#9ca3af" : "#6b7280", flexShrink: 0, color: darkMode ? "#9ca3af" : "#6b7280", flexShrink: 0,
}; };
const flyoutPanelStyle: React.CSSProperties = { const flyoutPanelStyle: React.CSSProperties = {
position: "fixed", left: "48px", position: "fixed", left: `${collapsedRailWidth + 4}px`,
top: `${Math.min(flyoutY, (typeof window !== "undefined" ? window.innerHeight : 800) - 260)}px`, top: `${Math.min(flyoutY, (typeof window !== "undefined" ? window.innerHeight : 800) - 260)}px`,
background: darkMode ? "#1a1a2e" : "var(--paper)", background: darkMode ? "#1a1a2e" : "var(--paper)",
border: "1px solid var(--line)", borderRadius: "10px", padding: "12px 14px", border: "1px solid var(--line)", borderRadius: "10px", padding: "12px 14px",
@ -5521,7 +5551,7 @@ export default function WeeklyView() {
if (flyoutTimerRef.current) clearTimeout(flyoutTimerRef.current); if (flyoutTimerRef.current) clearTimeout(flyoutTimerRef.current);
}; };
const railSep = ( const railSep = (
<div style={{ height: "1px", background: "var(--line)", width: "28px", margin: "3px 0", flexShrink: 0, alignSelf: "center" }} /> <div style={{ height: "1px", background: "var(--line)", width: compactRail ? "22px" : "28px", margin: compactRail ? "1px 0" : "3px 0", flexShrink: 0, alignSelf: "center" }} />
); );
// All-Day Events Section (reusable for above/below positioning) // All-Day Events Section (reusable for above/below positioning)
@ -5535,7 +5565,7 @@ export default function WeeklyView() {
const handleOnTop = effectiveAllDayPosition === "below"; const handleOnTop = effectiveAllDayPosition === "below";
const resizeHandle = isAllDayExpanded ? ( const resizeHandle = isAllDayExpanded ? (
<div <div
className="resize-handle" className="resize-handle resize-handle-allday"
onMouseDown={(e) => startResize(e, 'allday', handleOnTop)} onMouseDown={(e) => startResize(e, 'allday', handleOnTop)}
onTouchStart={(e) => startResize(e, 'allday', handleOnTop)} onTouchStart={(e) => startResize(e, 'allday', handleOnTop)}
> >
@ -5830,10 +5860,10 @@ export default function WeeklyView() {
{useLeftRail && ( {useLeftRail && (
<> <>
{/* The rail panel */} {/* The rail panel */}
<div style={{ position: "fixed", left: 0, top: 0, bottom: 0, width: leftRailExpanded ? "240px" : "44px", transition: "width 0.2s ease", background: darkMode ? "#1a1a2e" : "var(--paper)", borderRight: "1px solid var(--line)", zIndex: 100, display: "flex", flexDirection: "column", overflow: "hidden" }}> <div className={`weekly-left-sidebar ${leftRailExpanded ? "expanded" : "collapsed"} ${compactRail ? "compact-height" : ""}`} style={{ position: "fixed", left: 0, top: 0, bottom: 0, width: leftRailExpanded ? `${expandedRailWidth}px` : `${collapsedRailWidth}px`, transition: "width 0.2s ease", background: darkMode ? "#1a1a2e" : "var(--paper)", borderRight: "1px solid var(--line)", zIndex: 100, display: "flex", flexDirection: "column", overflow: "hidden" }}>
{leftRailExpanded ? ( {leftRailExpanded ? (
// ── EXPANDED MODE ── // ── EXPANDED MODE ──
<div style={{ padding: "16px 14px", display: "flex", flexDirection: "column", gap: "10px", overflowY: "auto", flex: 1 }}> <div style={{ padding: compactRail ? "10px 10px" : "16px 14px", display: "flex", flexDirection: "column", gap: compactRail ? "6px" : "10px", overflowY: "auto", flex: 1 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontSize: "0.85rem", fontWeight: 700, color: darkMode ? "#e5e7eb" : "#333" }}>{profile.language === "de" ? "Einstellungen" : "Preferences"}</span> <span style={{ fontSize: "0.85rem", fontWeight: 700, color: darkMode ? "#e5e7eb" : "#333" }}>{profile.language === "de" ? "Einstellungen" : "Preferences"}</span>
<button onClick={() => setLeftRailExpanded(false)} style={{ background: "none", border: "none", cursor: "pointer", color: darkMode ? "#6b7280" : "#9ca3af", padding: "2px", display: "flex" }} title="Collapse"> <button onClick={() => setLeftRailExpanded(false)} style={{ background: "none", border: "none", cursor: "pointer", color: darkMode ? "#6b7280" : "#9ca3af", padding: "2px", display: "flex" }} title="Collapse">
@ -5902,7 +5932,7 @@ export default function WeeklyView() {
</div> </div>
) : ( ) : (
// ── COLLAPSED MODE — icon strip ── // ── COLLAPSED MODE — icon strip ──
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", paddingTop: "6px", paddingBottom: "8px", height: "100%", overflowY: "auto", overflowX: "visible" }}> <div style={{ display: "flex", flexDirection: "column", alignItems: "center", paddingTop: compactRail ? "4px" : "6px", paddingBottom: compactRail ? "4px" : "8px", height: "100%", overflowY: compactRail ? "hidden" : "auto", overflowX: "visible" }}>
{/* Expand toggle */} {/* Expand toggle */}
<button onClick={() => setLeftRailExpanded(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Erweitern" : "Expand"}><PanelLeftOpen size={16} /></button> <button onClick={() => setLeftRailExpanded(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Erweitern" : "Expand"}><PanelLeftOpen size={16} /></button>
{railSep} {railSep}
@ -6243,7 +6273,7 @@ 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-1.5 border-b bg-white dark:bg-gray-900 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : { borderBottomColor: 'var(--line)', minHeight: useLeftRail ? "44px" : undefined }}> <header className="group relative flex items-center justify-between w-full px-4 py-1.5 border-b bg-white dark:bg-gray-900 dark:text-white transition-colors duration-200" style={isMobile ? { display: "none" } : { borderBottomColor: 'var(--line)', minHeight: useLeftRail ? (compactRail ? "40px" : "44px") : undefined }}>
{/* LEFT SECTION: View Switcher, Days, Hours, Slot Duration — only shown in top-toolbar mode */} {/* LEFT SECTION: View Switcher, Days, Hours, Slot Duration — only shown in top-toolbar mode */}
<div className={`weekly-header-controls flex items-center gap-4 transition-opacity duration-700 ${!useLeftRail ? "opacity-0 group-hover:opacity-100" : ""}`} style={{ zIndex: 1, display: useLeftRail ? "none" : undefined, ...(showIntroHints ? { opacity: 1 } : {}) }}> <div className={`weekly-header-controls flex items-center gap-4 transition-opacity duration-700 ${!useLeftRail ? "opacity-0 group-hover:opacity-100" : ""}`} style={{ zIndex: 1, display: useLeftRail ? "none" : undefined, ...(showIntroHints ? { opacity: 1 } : {}) }}>
{/* Quick-settings button (top mode only) */} {/* Quick-settings button (top mode only) */}
@ -6503,8 +6533,9 @@ export default function WeeklyView() {
</div> </div>
{/* RIGHT SECTION: Navigation & Tools */} {/* RIGHT SECTION: Navigation & Tools */}
<div className="weekly-header-controls flex-shrink-0 flex items-center gap-1.5 sm:gap-2 transition-opacity duration-700 opacity-0 group-hover:opacity-100" style={{ zIndex: 10, display: useLeftRail ? "none" : undefined, ...(showIntroHints ? { opacity: 1 } : {}) }}> <div className={`weekly-header-controls flex-shrink-0 flex items-center gap-1.5 sm:gap-2 transition-opacity duration-700 ${useLeftRail ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`} style={{ zIndex: 10, marginLeft: useLeftRail ? "auto" : undefined, display: useLeftRail && !showHeaderControls ? "none" : undefined, ...(showIntroHints ? { opacity: 1 } : {}) }}>
{/* Secondary actions — hidden on tablet, visible on desktop */} {/* Secondary actions — hidden on tablet, visible on desktop */}
{!useLeftRail && (
<div className="header-desktop-only flex items-center gap-1.5"> <div className="header-desktop-only flex items-center gap-1.5">
<button onClick={handleUndo} disabled={undoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Undo (Ctrl+Z)"> <button onClick={handleUndo} disabled={undoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Undo (Ctrl+Z)">
<Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" /> <Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
@ -6525,6 +6556,7 @@ export default function WeeklyView() {
{darkMode ? <Sun size={17} className="text-yellow-500" /> : <Moon size={17} className="text-gray-500" />} {darkMode ? <Sun size={17} className="text-yellow-500" /> : <Moon size={17} className="text-gray-500" />}
</button> </button>
</div> </div>
)}
{/* Navigation Controls — always visible */} {/* Navigation Controls — always visible */}
<div className="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5"> <div className="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
@ -6540,7 +6572,7 @@ export default function WeeklyView() {
<button className="weekly-btn-icon" onClick={() => setShowSettings(true)} title="Settings"><Settings size={17} /></button> <button className="weekly-btn-icon" onClick={() => setShowSettings(true)} title="Settings"><Settings size={17} /></button>
{/* Overflow menu — visible on tablet, hidden on desktop */} {/* Overflow menu — visible on tablet, hidden on desktop */}
<div className="header-tablet-only" style={{ position: "relative" }}> <div className={useLeftRail ? "" : "header-tablet-only"} style={{ position: "relative" }}>
<button className="weekly-btn-icon" onClick={() => setShowHeaderMore(!showHeaderMore)} title="More"> <button className="weekly-btn-icon" onClick={() => setShowHeaderMore(!showHeaderMore)} title="More">
<MoreVertical size={17} /> <MoreVertical size={17} />
</button> </button>
@ -6589,9 +6621,13 @@ export default function WeeklyView() {
</div> </div>
{/* Desktop only: Recurring Tasks + New Project */} {/* Desktop only: Recurring Tasks + New Project */}
{!useLeftRail && (
<>
<button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button> <button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button>
<button className="weekly-btn-icon header-desktop-only" onClick={() => setShowProjectsSidebar(true)} title="New Project"><FolderPlus size={17} /></button> <button className="weekly-btn-icon header-desktop-only" onClick={() => setShowProjectsSidebar(true)} title="New Project"><FolderPlus size={17} /></button>
<button className="weekly-btn-icon header-desktop-only" onClick={() => setShowWeekPrintModal(true)} title={profile.language === "de" ? "Woche drucken / exportieren" : "Print / export week"}><Printer size={17} /></button> <button className="weekly-btn-icon header-desktop-only" onClick={() => setShowWeekPrintModal(true)} title={profile.language === "de" ? "Woche drucken / exportieren" : "Print / export week"}><Printer size={17} /></button>
</>
)}
{/* User Menu */} {/* User Menu */}
<UserMenu <UserMenu
@ -8031,7 +8067,7 @@ export default function WeeklyView() {
{/* Resize handle - on top border of someday section */} {/* Resize handle - on top border of someday section */}
{somedayExpanded && ( {somedayExpanded && (
<div <div
className="resize-handle" className="resize-handle resize-handle-anyday"
onMouseDown={(e) => startResize(e, 'someday', true)} onMouseDown={(e) => startResize(e, 'someday', true)}
onTouchStart={(e) => startResize(e, 'someday', true)} onTouchStart={(e) => startResize(e, 'someday', true)}
> >
@ -11782,5 +11818,3 @@ function NotesSidebar({ task, onClose, updateTaskNotes, updateTaskUrl }: NotesSi
</> </>
); );
} }