Compare commits

..

No commits in common. "4a1802706868adc6b104502c72f6d2913e31fe39" and "4a5910779ea7613134a8bb5478f03449211198eb" have entirely different histories.

10 changed files with 81 additions and 698 deletions

View File

5
.gitignore vendored
View File

@ -43,7 +43,4 @@ prisma/dev.db-journal
certificates
# Design reference files (not tracked)
docs/design/
# Dokumentationen (nicht tracken)
APP_ERKLÄRUNG.md
docs/design/

View File

@ -1,255 +0,0 @@
# Design Notes — todo.martin-bierschenk.de v2
> Visual reference: `Todo Calendar Redesign v2.html`
> Aesthetic: Minimal Scandinavian / TeuxDeux-style hairline editorial
> Goal: a calm, airy, paper-like weekly planner. **Restyle only — no logic, routing, data, sync, or component-API changes.**
---
## 0 · Rules of engagement (read first)
1. **Do not change behaviour.** Keep all existing hooks, server actions, NextAuth wiring, calendar sync, recurring-task logic, drag-and-drop, keyboard shortcuts, and component prop shapes exactly as they are.
2. **Touch JSX + CSS only.** New CSS variables / Tailwind tokens are fine; new state, new effects, new dependencies are not.
3. **One component per commit.** Visual restyles are easy to review (and revert) when scoped. Suggested order: tokens → layout shell → day grid → all-day strip → events → tasks (anyday) → left rail → right settings → header.
4. **Keep existing component file structure** (`WeeklyCalendarView.tsx`, `TaskList.tsx`, `CalendarEventModal.tsx`, etc.). Only edit them.
5. **Where this doc and the HTML mock disagree, the mock wins** for visuals. Where the mock and the existing code disagree on behaviour, the existing code wins.
---
## 1 · Design tokens
Add these as CSS custom properties (or extend `tailwind.config.js` `theme.extend`). All other colors should be derived from these.
### Color
keep the colors for now
**Calendar-source palette** (event chips, muted on purpose — they should not compete with the today-red):
| Source | Background | Border-left | Title color | Meta color |
|---------------|-------------|-------------|-------------|------------|
| Default/work | `#eef2f7` | `#6c87a8` | `#2b3f57` | `#6c87a8` |
| Family (`.fam`) | `#fbeef0` | `#b85a6a` | `#5a2530` | `#99536a` |
| Finance (`.fin`)| `#f6f0e2` | `#a88a3c` | `#4a3a14` | `#8a6f2a` |
| Dev/GitHub (`.dev`) | `#ecf3ed` | `#5a7a4a` | `#2c4527` | `#5a7a4a` |
| Note (`.note`) | transparent | none | `var(--ink-2)` | `var(--ink-4)` |
**All-day chip palette** (solid pills above the day headers):
| Variant | Background | Text |
|--------------|-------------|-----------|
| Default | `#b8b8b0` | `#fff` |
| Family (`.fam`) | `#e88a8a` | `#fff` |
| Special (`.special`, anniversaries, milestones) | `#f4d588` | `#6b4f10` |
### Typography
- **UI / sans**: `Geist`, fallback `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif`
- **Editorial / serif**: `Fraunces` italic — used **only** for the small quote in the header bar
- **Mono / numerals**: `Geist Mono` if needed; otherwise rely on `font-variant-numeric: tabular-nums` for any clock/date/count
Body letter-spacing: `-0.005em` baseline.
| Role | Size | Weight | LH | Other |
|-----------------------|------|--------|-------|-------|
| Section h3 | 16px | 600 | 1.2 | |
| Day name (`.dn`) | 16px | 600 | 1 | uppercase, `letter-spacing: 0.04em` |
| Day name — today | 16px | 700 | 1 | color: `--accent` |
| Day date (`.dnum`) | 11px | 400 | 1 | uppercase, `tabular-nums`, color: `--ink-4` |
| Header KW (`.wk`) | 13px | 500 | — | `letter-spacing: 0.02em` |
| Header quote | 12px | 400 | — | Fraunces italic, `--ink-4` |
| Event title | 12px | 500 | 1.25 | |
| Event note title | 11.5px | 400 | 1.25 | |
| Event meta / time | 10.5px | 400 | — | `tabular-nums` |
| All-day chip | 11px | 500 | 1.2 | |
| List heading (`h4`) | 12px | 500 | — | uppercase, `letter-spacing: 0.04em` |
| List item | 12px | 400 | 1.35 | |
| Hour label | 10px | 400 | — | `tabular-nums`, color: `--ink-5` |
| Tasks tab | 12px | 500/600 | — | active is 600 |
| Tab counter chip | 10.5px | 500 | 1.4 | `tabular-nums` |
### Spacing & geometry
- Left rail: **56px** wide, fixed
- Right settings drawer: **360px** wide, slides in from the right (`transform: translateX(100%)` → `translateX(0)`, `cubic-bezier(.2,.8,.2,1)` 250ms)
- Permanent top header: **36px** tall, white, hairline bottom
- Day grid hour-row height: **56px**
- Day grid column gap: **20px**
- Day grid horizontal padding: **24px**
- Border radius: 4px (events), 6px (corner buttons, inputs), 78px (list cards, segmented pills), 10px (tasks tab pill container)
- All shadows are tiny: `0 1px 2px rgba(0,0,0,.06)` or `0 4px 16px rgba(0,0,0,.06)`. Never bigger.
- Hover surfaces: `rgba(0,0,0,.05)` for icons, `rgba(0,0,0,.025).04` for text hits
### Motion
- All transitions: `120180ms`, `cubic-bezier(.2,.8,.2,1)` for sliding panels, plain `ease` for hover
- The right drawer is the only large transform; everything else is opacity / background swaps
---
## 2 · Layout shell
```
┌─────────────────────────── header (36px, always visible) ───┐
│ KW 19 | 410. Mai 2026 "quote…" │
├──────┬────────────────────────────────────────────────┬─────┤
│ │ all-day strip │ │
│ left │────────────────────────────────────────────────│ rt │
│ rail │ day-headers row │ drw │
│ 56px │────────────────────────────────────────────────│ 360 │
│ │ day grid (hours × columns) │ px │
│ │────────────────────────────────────────────────│ off │
│ │ tasks (anyday) — warm grey, 6 white list cards │ scr │
└──────┴────────────────────────────────────────────────┴─────┘
```
- The **toolbar is gone**. Only two floating icon buttons remain: a sidebar-toggle top-left of the header, a search top-right.
- The right drawer is hidden by default. A small `18×64px` chevron tab on the right edge toggles it; the gear icon in the left rail also toggles it.
---
## 3 · Component-by-component restyle guide
### 3.1 Top header (`header`)
- `position: sticky`/`absolute`, `top: 0`, height 36px, white, hairline bottom
- Centered: `KW WW | 410. Mai 2026` • faint Fraunces italic quote
- Text colors: KW = `--ink-2` 13px/500; quote = `--ink-4` 12px italic; ellipsize at ~480px
### 3.2 Left rail (`lside`)
Order, top to bottom:
1. **Date nav cluster**` row, "Heute" pill (uppercase, hairline border), ` »` row
2. Hairline separator
3. **View flyout** — calendar icon; on hover, flyout reveals: Einfach / Kalender / Liste / Kanban / Ziel
4. **Day-count flyout** — grid icon; flyout: 1 / 2 / 3 / 5 / 7
5. **Zeitfenster flyout** — clock icon; flyout: 15m / 20m / 30m / 60m
6. Hairline separator
7. Action icons (vertical stack): plus, calendar-pick, projects, recurring, target, focus
8. Hairline separator
9. Undo, redo, sync, download, print
10. Hairline separator
11. **Light/dark toggle** (single sun/moon icon — toggles, not two icons)
12. **User avatar** at the bottom (`margin-top: auto`)
Icons are 16px, currentColor, in 36×36 hit-targets, `border-radius: 8px`, hover background `rgba(0,0,0,.05)`. Active state uses `--accent-soft` background + `--accent` color.
Flyouts pop **right** of the rail with a subtle pointer triangle.
### 3.3 All-day strip (`allday`)
- Grid: `56px [hour gutter pad] repeat(N, 1fr)`, column gap 20px, padding `10px 24px 0`
- Per-day column: `min-height: 56px`, vertical stack of chips, gap 3px
- Chips are **solid colored pills** (see palette table). No icons, no date pips inside.
- Today's column gets `background: var(--accent-soft)` with extra horizontal padding
- Small uppercase label "GANZTAGS" 9px in left gutter
### 3.4 Day headers (`days` + `dayh`)
- Grid identical to all-day strip (same column gaps for alignment)
- Each header: small **MAI 6** date on top, large **MITTWOCH** weekday name below, both uppercase
- 2px solid `--ink` underline below each header
- **Today**: weekday in `--accent` red 700, blue-grey `--accent-soft` background, centered, padding 6px 8px, 1px ink underline
- **Weekend (Sat/Sun)**: weekday red, underline red
### 3.5 Day grid body (`body`)
- **No vertical lines** between days — the column gap (20px) is the separator. This is the TeuxDeux move.
- Horizontal hour rules only (`--line` color, 1px), one per hour
- Hour labels in the left gutter, 10px, `--ink-5`, right-aligned, sitting on the rule (background-cut)
- **Today column** gets `background: var(--accent-soft)` (full height)
- **Now-line**: 1px `--accent` red across the today column, with an 8×8 dot on the left and a small time pill (`HH:MM`) on the right with `bg: var(--bg)` for clean cutout
### 3.6 Events (`ev`)
Two visual modes:
**Timed event card** — coloured background by source, 2px left-border accent, `border-radius: 4px`, `padding: 8px 10px`. Title row has an inline 16px source icon (`.t .src`) before the text. Synced events show a small sync arrow at top-right (`.sync`, opacity .55). Meta line: `HH:MM HH:MM · Location`.
**Note event** (`.ev.note`) — for todos placed at a time but without a duration block: transparent background, no border-left, no rounding, just small inline icon + text. Used for github-source items, key/cake/car category notes, etc.
Source icon mapping (16px, currentColor SVG):
- `git` — github / dev tasks
- `money` — finance / banking
- `book` — Legasthenie / Nachhilfe
- `home` — Garde / home
- `key`, `cake`, `car` — category notes
- `bolt` — focus / quick task
- `pin`, `info` — generic
### 3.7 Anyday tasks (`tasks`)
- Section background: `--tasks-bg` (`#f4f2ee` warm light grey)
- 6-up grid of white list cards (`--paper`, 1px `--line`, 8px radius, `min-height: 200px`, padding `12px 12px 8px`)
- Card heading: 6px colored dot · uppercase project name · count chip on right
- Each item: 9×9 unchecked box (1px `--ink-5`) · text · hairline `--line-soft` underneath
- **Tabs above the grid** are a segmented-pill control:
- Container: `inline-flex`, `padding: 4px`, `bg: rgba(0,0,0,.05)`, `border-radius: 10px`
- Active tab: white pill, weight 600, shadow `0 1px 2px rgba(0,0,0,.06), 0 0 0 1px rgba(0,0,0,.04)`
- Inactive: text `--ink-3`, hover white-50 background
- Each tab has a count chip — small grey rounded-rect, tabular-nums
### 3.8 Right settings drawer (`rside`)
- 360px white panel, slides in from right edge
- Vertical icon-tab strip across the top (settings, globe, link, sync, box, user, palette, spark)
- Each row: small label (11px `--ink-3`, weight 500) + input/checkbox/segmented control
- Inputs: 1px `--line`, 6px radius, 7px 10px padding, 13px `--ink-2`
- Help text under fields: 10px `--ink-4`
- Sections separated by full-bleed `--line` `<hr>`
- Close `×` top-right; clicking the gear icon in the left rail or the edge tab toggles it
### 3.9 Floating corner buttons
- 28×28, 6px radius, `top: 8px`
- Left: sidebar-toggle. Right: search.
- These sit **on top of** the header bar.
---
## 4 · Iconography
All icons are **16px, currentColor, 1.4 stroke-width SVG**, drawn on a 20×20 viewBox, no fill (line-art). Examples: cal, list, kanban, target, grid, clock, search, plus, proj, repeat, bolt, moon, sun, setg, sync, print, user, download, sidebar, chevL/R, eye, undo, redo, refresh, sliders, globe, link, box, palette, spark, info, money, git, book, home, car, key, cake.
Hover icons go from `--ink-3``--ink`. Active state for view/filter icons goes from `--ink-3``--accent` on `--accent-soft` background.
---
## 5 · What to keep from the existing app
- All data shapes, server actions, API routes
- `next-auth` wiring, middleware
- Drag-and-drop calendar event editing
- Recurring task logic
- Calendar-sync flows (Google etc.)
- Keyboard shortcuts
- Print view (we can restyle it, but keep the print route)
---
## 6 · Suggested PR breakdown
1. **`feat(design): add tokens`** — CSS variables / tailwind extend, no markup changes
2. **`feat(design): app shell + header + corner buttons`** — top-level layout
3. **`feat(design): left rail`** — collapse old toolbar into rail with flyouts
4. **`feat(design): right settings drawer`** — slide-in panel
5. **`feat(design): day grid`** — hairline rules, no verticals, today wash, day headers
6. **`feat(design): all-day strip`** — solid chip pills
7. **`feat(design): events`** — source-coloured cards + note variant + source icons
8. **`feat(design): anyday tasks`** — segmented tabs + 6-up white cards on warm grey
9. **`chore(design): typography pass`** — Geist + Fraunces, sizes from §1
10. **`chore(design): icon set swap`** — 16px line-art set
Each PR small enough to eyeball. After step 9 the app should look like the mock; step 10 is polish.
---
## 7 · Open questions for the engineer
- Is Geist already loaded? If not, add it via `next/font` (don't pull from Google Fonts at runtime).
- Does the existing `theme` system support multiple accent colors per calendar source, or is that a new concept? If new, hold off on the per-source event colors and ship the today-red + neutral-event scheme first.
- Confirm dark mode is in scope — the mock is light-only; tokens for dark would need to be derived in a follow-up.
---
*End of notes. When in doubt, open `Todo Calendar Redesign v2.html` in a browser and copy what you see.*

View File

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

@ -3270,29 +3270,26 @@ h3 {
/* Resize handle for draggable section borders */
.resize-handle {
height: 2px;
border: 1px solid #ccc;
height: 7px;
cursor: ns-resize;
display: flex;
align-items: center;
justify-content: center;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
touch-action: none;
position: relative;
z-index: 50;
z-index: 10;
flex-shrink: 0;
overflow: visible;
background-color: #ddd;
border-top: 1px solid var(--weekly-border, #e5e7eb);
border-bottom: 1px solid var(--weekly-border, #e5e7eb);
}
.resize-handle:hover {
background-color: #d7d7d7;
border-color: #b8b8b8;
background: rgba(59, 130, 246, 0.08);
border-color: var(--weekly-accent, #3b82f6);
}
.resize-handle:active {
background-color: #d1d1d1;
border-color: #aaa;
background: rgba(59, 130, 246, 0.12);
border-color: var(--weekly-accent, #3b82f6);
}
.resize-handle-bar {
width: 32px;
@ -3302,50 +3299,10 @@ h3 {
opacity: 0.6;
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 1px / 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 1px / 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:active .resize-handle-bar {
opacity: 1;
}
.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;
background: var(--weekly-accent, #3b82f6);
opacity: 0.5;
}
.all-day-events-header {

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, lazy, Suspense, useRef, useCallback } from 'react';
import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity, Check } from 'lucide-react';
import React, { useState, useEffect, lazy, Suspense, useRef } from 'react';
import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity } from 'lucide-react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
import { faServer } from '@fortawesome/free-solid-svg-icons';
@ -27,7 +27,7 @@ function AnimatedDots() {
}
// Reminder preset options (minutes)
const REMINDER_PRESETS = [
const REMINDER_OPTIONS = [
{ label: 'None', value: -1 },
{ label: 'At time of event', value: 0 },
{ label: '5 minutes before', value: 5 },
@ -40,25 +40,6 @@ const REMINDER_PRESETS = [
{ label: '1 week before', value: 10080 },
];
const CUSTOM_REMINDER_SENTINEL = -9999;
function formatReminderMinutes(minutes: number, language: string): string {
if (minutes === 0) return language === 'de' ? 'Zum Zeitpunkt' : 'At time of event';
if (minutes % 10080 === 0) {
const w = minutes / 10080;
return language === 'de' ? `${w} ${w === 1 ? 'Woche' : 'Wochen'} vorher` : `${w} week${w !== 1 ? 's' : ''} before`;
}
if (minutes % 1440 === 0) {
const d = minutes / 1440;
return language === 'de' ? `${d} ${d === 1 ? 'Tag' : 'Tage'} vorher` : `${d} day${d !== 1 ? 's' : ''} before`;
}
if (minutes % 60 === 0) {
const h = minutes / 60;
return language === 'de' ? `${h} ${h === 1 ? 'Stunde' : 'Stunden'} vorher` : `${h} hour${h !== 1 ? 's' : ''} before`;
}
return language === 'de' ? `${minutes} Minuten vorher` : `${minutes} minutes before`;
}
const BUSY_STATUS_OPTIONS = [
{ label: 'Busy', value: 'busy' },
{ label: 'Free', value: 'free' },
@ -82,10 +63,6 @@ interface CalendarEventModalProps {
connections: any[];
weekStartDay?: number; // 0=Sunday, 1=Monday
language?: string;
savedLocations?: string[];
onSaveLocation?: (loc: string) => void;
customReminderMinutes?: number[];
onSaveCustomReminder?: (minutes: number) => void;
onClose: () => void;
onSave: (eventData: any) => Promise<void>;
onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise<void>;
@ -99,10 +76,6 @@ export default function CalendarEventModal({
connections,
weekStartDay = 0,
language = 'en',
savedLocations = [],
onSaveLocation,
customReminderMinutes = [],
onSaveCustomReminder,
onClose,
onSave,
onDelete
@ -194,53 +167,6 @@ export default function CalendarEventModal({
const calendarSelectorRef = useRef<HTMLDivElement>(null);
const dialogRef = useRef<HTMLDivElement>(null);
// Location autocomplete
const [locationFocused, setLocationFocused] = useState(false);
const [locationSuggestions, setLocationSuggestions] = useState<string[]>([]);
const locationRef = useRef<HTMLDivElement>(null);
const updateLocationSuggestions = useCallback((val: string) => {
if (!val.trim() || savedLocations.length === 0) {
setLocationSuggestions([]);
return;
}
const lower = val.toLowerCase();
const matches = savedLocations.filter(l => l.toLowerCase().includes(lower) && l !== val);
setLocationSuggestions(matches.slice(0, 6));
}, [savedLocations]);
// Custom reminder state
// customReminderIdx tracks which reminder row is showing the custom editor
const [customEditorIdx, setCustomEditorIdx] = useState<number | null>(null);
const [customAmount, setCustomAmount] = useState(30);
const [reminderUnit, setReminderUnit] = useState<'minutes' | 'hours' | 'days'>('minutes');
const customAmountToMinutes = () => {
if (reminderUnit === 'hours') return customAmount * 60;
if (reminderUnit === 'days') return customAmount * 1440;
return customAmount;
};
// Build full reminder options list: presets + saved custom + "Custom..."
const buildReminderOptions = () => [
...REMINDER_PRESETS,
...customReminderMinutes
.filter(m => !REMINDER_PRESETS.some(p => p.value === m))
.map(m => ({ label: formatReminderMinutes(m, language), value: m })),
{ label: language === 'de' ? 'Benutzerdefiniert…' : 'Custom…', value: CUSTOM_REMINDER_SENTINEL },
];
// Close location suggestions on outside click
useEffect(() => {
const handler = (e: MouseEvent) => {
if (locationRef.current && !locationRef.current.contains(e.target as Node)) {
setLocationSuggestions([]);
}
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
// Focus trap
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@ -335,11 +261,6 @@ export default function CalendarEventModal({
return;
}
// Save location to user profile if non-empty and new
if (location.trim() && onSaveLocation) {
onSaveLocation(location.trim());
}
setIsSaving(true);
setError('');
try {
@ -427,29 +348,11 @@ export default function CalendarEventModal({
const updateReminder = (index: number, minutes: number) => {
if (minutes === -1) {
setReminders(reminders.filter((_, i) => i !== index));
setCustomEditorIdx(null);
return;
}
if (minutes === CUSTOM_REMINDER_SENTINEL) {
setCustomEditorIdx(index);
setCustomAmount(30);
setReminderUnit('minutes');
return;
}
const updated = [...reminders];
updated[index] = { ...updated[index], minutes };
setReminders(updated);
setCustomEditorIdx(null);
};
const confirmCustomReminder = (index: number) => {
const mins = customAmountToMinutes();
if (mins <= 0) return;
const updated = [...reminders];
updated[index] = { ...updated[index], minutes: mins };
setReminders(updated);
setCustomEditorIdx(null);
if (onSaveCustomReminder) onSaveCustomReminder(mins);
};
const addReminder = () => {
@ -784,61 +687,24 @@ export default function CalendarEventModal({
</div>
)}
{/* Location with autocomplete */}
<div style={{ ...iconRow, position: 'relative' }} ref={locationRef}>
{/* Location */}
<div style={iconRow}>
<div style={iconCol} aria-hidden="true"><MapPin size={14} /></div>
<input
type="text"
value={location}
onChange={e => { setLocation(e.target.value); updateLocationSuggestions(e.target.value); }}
onFocus={() => { setLocationFocused(true); updateLocationSuggestions(location); }}
onBlur={() => setTimeout(() => setLocationSuggestions([]), 150)}
onKeyDown={e => { if (e.key === 'Escape') setLocationSuggestions([]); }}
onChange={e => setLocation(e.target.value)}
placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'}
aria-label={language === 'de' ? 'Ort' : 'Location'}
aria-autocomplete="list"
aria-expanded={locationSuggestions.length > 0}
style={{
...fieldCol, padding: '2px 0', border: 'none',
background: 'transparent', outline: 'none', fontSize: '0.8rem',
color: 'var(--weekly-text)',
}}
/>
{locationSuggestions.length > 0 && (
<div
role="listbox"
aria-label={language === 'de' ? 'Ortsvorschläge' : 'Location suggestions'}
style={{
position: 'absolute', top: '100%', left: '26px', right: 0,
zIndex: 200, background: 'var(--weekly-bg-popover, #fff)',
border: '1px solid var(--weekly-border, #e5e7eb)',
borderRadius: '8px', boxShadow: '0 4px 16px rgba(0,0,0,0.12)',
overflow: 'hidden', marginTop: '2px',
}}
>
{locationSuggestions.map((suggestion, i) => (
<div
key={i}
role="option"
aria-selected={false}
onMouseDown={e => { e.preventDefault(); setLocation(suggestion); setLocationSuggestions([]); }}
style={{
padding: '6px 10px', fontSize: '0.78rem',
cursor: 'pointer', color: 'var(--weekly-text)',
display: 'flex', alignItems: 'center', gap: '6px',
}}
onMouseEnter={e => (e.currentTarget.style.background = 'var(--weekly-hover, rgba(0,0,0,0.05))')}
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
>
<MapPin size={11} style={{ opacity: 0.4, flexShrink: 0 }} />
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{suggestion}</span>
</div>
))}
</div>
)}
</div>
{/* Alert / Reminders */}
{/* Alert */}
<div style={iconRow}>
<div style={iconCol} aria-hidden="true"><Bell size={14} /></div>
<div style={{ ...fieldCol, display: 'flex', flexDirection: 'column', gap: '2px' }}>
@ -848,79 +714,23 @@ export default function CalendarEventModal({
</button>
) : (
reminders.map((reminder, idx) => (
<div key={idx} style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<select
value={customEditorIdx === idx ? CUSTOM_REMINDER_SENTINEL : reminder.minutes}
onChange={e => updateReminder(idx, parseInt(e.target.value))}
aria-label={`${language === 'de' ? 'Erinnerung' : 'Reminder'} ${idx + 1}`}
style={{ ...inlineSelect, flex: 1 }}
>
{buildReminderOptions().map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<button
onClick={() => { setReminders(reminders.filter((_, i) => i !== idx)); if (customEditorIdx === idx) setCustomEditorIdx(null); }}
aria-label={language === 'de' ? `Erinnerung ${idx + 1} entfernen` : `Remove reminder ${idx + 1}`}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--weekly-text-light)', opacity: 0.5 }}>
<X size={12} aria-hidden="true" />
</button>
</div>
{/* Custom reminder editor row */}
{customEditorIdx === idx && (
<div style={{
display: 'flex', alignItems: 'center', gap: '4px',
paddingLeft: '0', background: 'var(--weekly-bg-secondary, #f3f4f6)',
borderRadius: '6px', padding: '4px 6px',
}}>
<input
type="number"
min={1}
max={9999}
value={customAmount}
onChange={e => setCustomAmount(Math.max(1, parseInt(e.target.value) || 1))}
aria-label={language === 'de' ? 'Erinnerungsmenge' : 'Reminder amount'}
style={{
width: '48px', textAlign: 'center', fontSize: '0.78rem',
background: 'var(--weekly-bg, #fff)', borderRadius: '5px',
padding: '2px 4px', border: '1px solid var(--weekly-border, #ddd)',
outline: 'none', color: 'var(--weekly-text)',
}}
/>
<select
value={reminderUnit}
onChange={e => setReminderUnit(e.target.value as any)}
aria-label={language === 'de' ? 'Erinnerungseinheit' : 'Reminder unit'}
style={{ ...inlineSelect, flex: 1, fontSize: '0.75rem' }}
>
<option value="minutes">{language === 'de' ? 'Minuten' : 'minutes'}</option>
<option value="hours">{language === 'de' ? 'Stunden' : 'hours'}</option>
<option value="days">{language === 'de' ? 'Tage' : 'days'}</option>
</select>
<span style={{ fontSize: '0.7rem', color: 'var(--weekly-text-light)', whiteSpace: 'nowrap' }}>
{language === 'de' ? 'vorher' : 'before'}
</span>
<button
onClick={() => confirmCustomReminder(idx)}
title={language === 'de' ? 'Bestätigen' : 'Confirm'}
style={{
background: '#3b82f6', border: 'none', borderRadius: '5px',
cursor: 'pointer', padding: '3px 7px', display: 'flex', alignItems: 'center',
color: '#fff', flexShrink: 0,
}}
>
<Check size={12} />
</button>
<button
onClick={() => setCustomEditorIdx(null)}
title={language === 'de' ? 'Abbrechen' : 'Cancel'}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--weekly-text-light)', opacity: 0.6 }}
>
<X size={12} />
</button>
</div>
)}
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<select
value={reminder.minutes}
onChange={e => updateReminder(idx, parseInt(e.target.value))}
aria-label={`${language === 'de' ? 'Erinnerung' : 'Reminder'} ${idx + 1}`}
style={{ ...inlineSelect, flex: 1 }}
>
{REMINDER_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<button
onClick={() => setReminders(reminders.filter((_, i) => i !== idx))}
aria-label={language === 'de' ? `Erinnerung ${idx + 1} entfernen` : `Remove reminder ${idx + 1}`}
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--weekly-text-light)', opacity: 0.5 }}>
<X size={12} aria-hidden="true" />
</button>
</div>
))
)}

View File

@ -299,7 +299,7 @@ export function GridTaskBlock({
if (editingTaskId !== task.id) toggleTask(task.id);
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "0.25rem", width: "100%", justifyContent: "space-between" }}>
<div style={{ display: "flex", alignItems: "flex-start", gap: "0.25rem", width: "100%", justifyContent: "space-between" }}>
{editingTaskId === task.id ? (
<form
onSubmit={(e) => {
@ -329,7 +329,7 @@ export function GridTaskBlock({
<span
style={{
display: "flex",
alignItems: "center",
alignItems: "flex-start",
gap: "3px",
overflow: "visible",
whiteSpace: "pre-wrap",
@ -342,7 +342,7 @@ export function GridTaskBlock({
}}
>
{(showPriorityIcons || showProjectIcons) && (
<span style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "14px", minWidth: "14px", flexShrink: 0, gap: "2px", opacity: task.completed && showTaskCheckboxes ? 0.5 : 1 }}>
<span style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", width: "14px", minWidth: "14px", flexShrink: 0, gap: "2px", paddingTop: "1px", opacity: task.completed && showTaskCheckboxes ? 0.5 : 1 }}>
{showPriorityIcons && (() => {
const pb = getPriorityBadge(task, priorityStyle, 11);
return pb ? (
@ -380,7 +380,7 @@ export function GridTaskBlock({
onChange={(e) => { e.stopPropagation(); toggleTask(task.id); }}
onClick={(e) => e.stopPropagation()}
className="task-checkbox flex-shrink-0"
style={{ width: "12px", height: "12px", margin: 0, cursor: "pointer", flexShrink: 0, accentColor: "#FFF" }}
style={{ width: "12px", height: "12px", margin: 0, cursor: "pointer", position: "relative", top: "3px", left: "-2px", accentColor: "#FFF" }}
/>
)}
<span style={task.completed && showTaskCheckboxes ? { opacity: 0.5, flex: 1, display: "flex", alignItems: "center", flexWrap: "wrap", rowGap: "2px" } : { flex: 1, display: "flex", alignItems: "center", flexWrap: "wrap", rowGap: "2px" }}>
@ -510,7 +510,7 @@ export function GridTaskBlock({
synology: { icon: faServer, color: "#007AFF", label: "Synology" },
};
return (
<span style={{ display: "flex", alignItems: "center", gap: "3px", flexShrink: 0 }}>
<span style={{ display: "flex", alignItems: "flex-start", gap: "3px", flexShrink: 0, paddingTop: "2px" }}>
{rolling && (
<span title="Auto-rolling task" style={{ display: "inline-flex", alignItems: "center", color: "var(--weekly-teal, #009a9a)" }}>
<svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">

View File

@ -1045,41 +1045,6 @@ function SettingsSidebar({
</button>
))}
</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>
{/* Header Display */}
<div>

View File

@ -128,7 +128,6 @@ type DeviceViewSettingKey = typeof DEVICE_VIEW_SETTINGS_KEYS[number];
// 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 VIEW_SETTINGS_PROFILE_KEYS = ["menuPosition", "showHeaderControls"];
function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
@ -919,7 +918,6 @@ export default function WeeklyView() {
// Mobile detection
const [isMobile, setIsMobile] = 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)
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
@ -1039,8 +1037,6 @@ export default function WeeklyView() {
weatherLon: null,
weatherLocation: "",
showCalendarProviderIcon: false,
menuPosition: "left",
showHeaderControls: true,
});
const [motivationalQuote, setMotivationalQuote] = useState("");
const [showSummary, setShowSummary] = useState(false);
@ -1261,8 +1257,6 @@ export default function WeeklyView() {
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
const datePickerBtnRef = useRef<HTMLDivElement>(null);
const [showNavDatePicker, setShowNavDatePicker] = useState(false);
const navDatePickerBtnRef = useRef<HTMLButtonElement>(null);
const [showQuickSettings, setShowQuickSettings] = useState(false);
const [leftRailExpanded, setLeftRailExpanded] = useState(false);
const [flyoutSection, setFlyoutSection] = useState<string | null>(null);
@ -1406,7 +1400,6 @@ export default function WeeklyView() {
const check = () => {
setIsMobile(window.innerWidth <= 768);
setIsPortrait(window.innerHeight > window.innerWidth);
setIsCompactHeight(window.innerHeight <= 720);
};
check();
window.addEventListener("resize", check);
@ -1423,12 +1416,7 @@ export default function WeeklyView() {
if (res.ok) {
const data = await res.json();
if (data && 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,
};
const profileData = data.user;
setProfile(profileData);
// Sync individual states to profile data
@ -2415,22 +2403,6 @@ export default function WeeklyView() {
somedayWheelCleanup.current = () => node.removeEventListener("wheel", handler);
}, []);
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
if (DEVICE_SETTINGS_KEYS.includes(key)) {
setCookie(`setting_${key}`, String(value));
@ -3187,9 +3159,6 @@ export default function WeeklyView() {
? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime }
: ev
));
} else {
const connId = getConnectionIdForCalendar(eventDragState.calendarId);
fetchCalendarEvents(true, connId);
}
} catch {
setRawCalendarEvents(prev => prev.map(ev =>
@ -3209,7 +3178,7 @@ export default function WeeklyView() {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [eventDragState, effectiveCellDuration, calendarEvents, fetchCalendarEvents, getConnectionIdForCalendar]);
}, [eventDragState, effectiveCellDuration, calendarEvents]);
// Slot drag-to-create: mousemove + mouseup on document (always active, ref-gated)
const effectiveCellDurationRef = useRef(effectiveCellDuration);
@ -3459,9 +3428,6 @@ export default function WeeklyView() {
setRawCalendarEvents(prev => prev.map(ev =>
ev.id === eventId ? { ...ev, startTime: originalStartTime, endTime: originalEndTime } : ev
));
} else {
const connId = getConnectionIdForCalendar(calendarId);
fetchCalendarEvents(true, connId);
}
} catch {
setRawCalendarEvents(prev => prev.map(ev =>
@ -5417,10 +5383,6 @@ export default function WeeklyView() {
};
const activeTheme = (darkMode ? profile.darkTheme : profile.lightTheme) as Record<string, string> | null;
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 = {
...(activeTheme ? {
@ -5499,7 +5461,7 @@ export default function WeeklyView() {
"--weekly-past-color": activeTheme?.color8 || (darkMode
? invertColor(profile.pastDayColor || "#a6a6a7")
: profile.pastDayColor || "#a6a6a7"),
...(useLeftRail ? { paddingLeft: leftRailExpanded ? `${expandedRailWidth}px` : `${collapsedRailWidth}px`, transition: "padding-left 0.2s ease" } : {}),
...(useLeftRail ? { paddingLeft: leftRailExpanded ? "240px" : "44px", transition: "padding-left 0.2s ease" } : {}),
} as React.CSSProperties;
if (isLoading) {
@ -5530,12 +5492,12 @@ export default function WeeklyView() {
});
const railIconBtnStyle: React.CSSProperties = {
width: compactRail ? "30px" : "36px", height: compactRail ? "30px" : "36px", display: "flex", alignItems: "center", justifyContent: "center",
background: "none", border: "none", cursor: "pointer", borderRadius: compactRail ? "7px" : "8px",
width: "36px", height: "36px", display: "flex", alignItems: "center", justifyContent: "center",
background: "none", border: "none", cursor: "pointer", borderRadius: "8px",
color: darkMode ? "#9ca3af" : "#6b7280", flexShrink: 0,
};
const flyoutPanelStyle: React.CSSProperties = {
position: "fixed", left: `${collapsedRailWidth + 4}px`,
position: "fixed", left: "48px",
top: `${Math.min(flyoutY, (typeof window !== "undefined" ? window.innerHeight : 800) - 260)}px`,
background: darkMode ? "#1a1a2e" : "var(--paper)",
border: "1px solid var(--line)", borderRadius: "10px", padding: "12px 14px",
@ -5559,7 +5521,7 @@ export default function WeeklyView() {
if (flyoutTimerRef.current) clearTimeout(flyoutTimerRef.current);
};
const railSep = (
<div style={{ height: "1px", background: "var(--line)", width: compactRail ? "22px" : "28px", margin: compactRail ? "1px 0" : "3px 0", flexShrink: 0, alignSelf: "center" }} />
<div style={{ height: "1px", background: "var(--line)", width: "28px", margin: "3px 0", flexShrink: 0, alignSelf: "center" }} />
);
// All-Day Events Section (reusable for above/below positioning)
@ -5573,7 +5535,7 @@ export default function WeeklyView() {
const handleOnTop = effectiveAllDayPosition === "below";
const resizeHandle = isAllDayExpanded ? (
<div
className="resize-handle resize-handle-allday"
className="resize-handle"
onMouseDown={(e) => startResize(e, 'allday', handleOnTop)}
onTouchStart={(e) => startResize(e, 'allday', handleOnTop)}
>
@ -5868,10 +5830,10 @@ export default function WeeklyView() {
{useLeftRail && (
<>
{/* The rail panel */}
<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" }}>
<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" }}>
{leftRailExpanded ? (
// ── EXPANDED MODE ──
<div style={{ padding: compactRail ? "10px 10px" : "16px 14px", display: "flex", flexDirection: "column", gap: compactRail ? "6px" : "10px", overflowY: "auto", flex: 1 }}>
<div style={{ padding: "16px 14px", display: "flex", flexDirection: "column", gap: "10px", overflowY: "auto", flex: 1 }}>
<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>
<button onClick={() => setLeftRailExpanded(false)} style={{ background: "none", border: "none", cursor: "pointer", color: darkMode ? "#6b7280" : "#9ca3af", padding: "2px", display: "flex" }} title="Collapse">
@ -5940,7 +5902,7 @@ export default function WeeklyView() {
</div>
) : (
// ── COLLAPSED MODE — icon strip ──
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", paddingTop: compactRail ? "4px" : "6px", paddingBottom: compactRail ? "4px" : "8px", height: "100%", overflowY: compactRail ? "hidden" : "auto", overflowX: "visible" }}>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", paddingTop: "6px", paddingBottom: "8px", height: "100%", overflowY: "auto", overflowX: "visible" }}>
{/* Expand toggle */}
<button onClick={() => setLeftRailExpanded(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Erweitern" : "Expand"}><PanelLeftOpen size={16} /></button>
{railSep}
@ -5970,9 +5932,8 @@ export default function WeeklyView() {
{railSep}
{/* History flyout (undo / redo) */}
<button style={{ ...railIconBtnStyle, opacity: undoCount === 0 && redoCount === 0 ? 0.5 : 1 }} title={profile.language === "de" ? "Verlauf" : "History"} onMouseEnter={(e) => openFlyout("history", e)} onMouseLeave={closeFlyoutDelayed}><Undo2 size={15} /></button>
{/* Refresh + Print */}
{/* Refresh */}
<button onClick={() => { fetchCalendarEvents(true); fetchTasks(); }} style={{ ...railIconBtnStyle }} title="Refresh"><RefreshCcw size={15} /></button>
<button onClick={() => setShowWeekPrintModal(true)} style={{ ...railIconBtnStyle }} title={profile.language === "de" ? "Drucken" : "Print"}><Printer size={15} /></button>
<div style={{ flex: 1 }} />
{railSep}
{/* Settings (cogwheel) + User menu at bottom */}
@ -6281,7 +6242,7 @@ export default function WeeklyView() {
)}
{/* 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 ? (compactRail ? "40px" : "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 ? "44px" : undefined }}>
{/* 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 } : {}) }}>
{/* Quick-settings button (top mode only) */}
@ -6377,15 +6338,6 @@ export default function WeeklyView() {
</div>
)}
{/* Jump to Date — mirrored from nav cluster */}
<button
className="p-1 hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm rounded text-gray-500 hover:text-black dark:hover:text-white transition-all"
onClick={() => setShowDatePicker(true)}
title={profile.language === "de" ? "Datum wählen" : "Jump to date"}
>
<CalendarDays size={15} />
</button>
</div>
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */}
@ -6550,50 +6502,27 @@ export default function WeeklyView() {
</div>
{/* 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 ${useLeftRail ? "opacity-100" : "opacity-0 group-hover:opacity-100"}`} style={{ zIndex: 10, marginLeft: useLeftRail ? "auto" : undefined, display: useLeftRail && !showHeaderControls ? "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 opacity-0 group-hover:opacity-100" style={{ zIndex: 10, display: useLeftRail ? "none" : undefined, ...(showIntroHints ? { opacity: 1 } : {}) }}>
{/* Secondary actions — hidden on tablet, visible on desktop */}
{!useLeftRail && (
<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)">
<Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
</button>
<button onClick={handleRedo} disabled={redoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Redo (Ctrl+Y)">
<Redo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
</button>
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} className="weekly-btn-icon" title="Add Calendar Event">
<Plus size={17} className="text-gray-600 hover:text-black transition-colors" />
</button>
<button onClick={() => { const newVal = !profile.showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); }} className={`weekly-btn-icon ${profile.showNextTask ? "active" : ""}`} title={profile.showNextTask ? "Showing Next Task" : "Showing Goal"}>
{profile.showNextTask ? <Play size={17} className="text-teal-600" /> : <Target size={17} className="text-gray-400" />}
</button>
<button onClick={() => setShowFocusMode(true)} className="weekly-btn-icon" title="Enter Focus Mode">
<Zap size={17} className="text-gray-600 hover:text-yellow-500 transition-colors" />
</button>
<button onClick={() => setDarkMode(!darkMode)} className="weekly-btn-icon" title={darkMode ? "Light Mode" : "Dark Mode"}>
{darkMode ? <Sun size={17} className="text-yellow-500" /> : <Moon size={17} className="text-gray-500" />}
</button>
</div>
)}
{/* Jump to Date — left of nav, picker opens below */}
<div style={{ position: "relative" }}>
<button
ref={navDatePickerBtnRef}
className="weekly-btn-icon"
onClick={() => setShowNavDatePicker(v => !v)}
title={profile.language === "de" ? "Datum wählen" : "Jump to date"}
>
<Calendar size={17} />
<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)">
<Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
</button>
<button onClick={handleRedo} disabled={redoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Redo (Ctrl+Y)">
<Redo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
</button>
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} className="weekly-btn-icon" title="Add Calendar Event">
<Plus size={17} className="text-gray-600 hover:text-black transition-colors" />
</button>
<button onClick={() => { const newVal = !profile.showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); }} className={`weekly-btn-icon ${profile.showNextTask ? "active" : ""}`} title={profile.showNextTask ? "Showing Next Task" : "Showing Goal"}>
{profile.showNextTask ? <Play size={17} className="text-teal-600" /> : <Target size={17} className="text-gray-400" />}
</button>
<button onClick={() => setShowFocusMode(true)} className="weekly-btn-icon" title="Enter Focus Mode">
<Zap size={17} className="text-gray-600 hover:text-yellow-500 transition-colors" />
</button>
<button onClick={() => setDarkMode(!darkMode)} className="weekly-btn-icon" title={darkMode ? "Light Mode" : "Dark Mode"}>
{darkMode ? <Sun size={17} className="text-yellow-500" /> : <Moon size={17} className="text-gray-500" />}
</button>
{showNavDatePicker && !isMobile && (
<SimpleDatePicker
selected={currentWeekStart}
onSelect={(date) => { setCurrentWeekStart(getStartOfWeek(date)); setShowNavDatePicker(false); }}
onClose={() => setShowNavDatePicker(false)}
language={profile.language}
anchorRef={navDatePickerBtnRef}
/>
)}
</div>
{/* Navigation Controls — always visible */}
@ -6610,7 +6539,7 @@ export default function WeeklyView() {
<button className="weekly-btn-icon" onClick={() => setShowSettings(true)} title="Settings"><Settings size={17} /></button>
{/* Overflow menu — visible on tablet, hidden on desktop */}
<div className={useLeftRail ? "" : "header-tablet-only"} style={{ position: "relative" }}>
<div className="header-tablet-only" style={{ position: "relative" }}>
<button className="weekly-btn-icon" onClick={() => setShowHeaderMore(!showHeaderMore)} title="More">
<MoreVertical size={17} />
</button>
@ -6659,13 +6588,9 @@ export default function WeeklyView() {
</div>
{/* 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={() => 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={() => 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={() => setShowWeekPrintModal(true)} title={profile.language === "de" ? "Woche drucken / exportieren" : "Print / export week"}><Printer size={17} /></button>
{/* User Menu */}
<UserMenu
@ -8105,7 +8030,7 @@ export default function WeeklyView() {
{/* Resize handle - on top border of someday section */}
{somedayExpanded && (
<div
className="resize-handle resize-handle-anyday"
className="resize-handle"
onMouseDown={(e) => startResize(e, 'someday', true)}
onTouchStart={(e) => startResize(e, 'someday', true)}
>
@ -9289,24 +9214,6 @@ export default function WeeklyView() {
connections={connections}
weekStartDay={profile.weekStartDay ?? 1}
language={profile.language}
savedLocations={(profile.viewSettings as any)?.savedLocations || []}
customReminderMinutes={(profile.viewSettings as any)?.customReminderMinutes || []}
onSaveLocation={(loc: string) => {
const current: string[] = (profile.viewSettings as any)?.savedLocations || [];
if (current.includes(loc)) return;
const updated = [loc, ...current].slice(0, 20);
const newVS = { ...(profile.viewSettings as any), savedLocations: updated };
setProfile((p: any) => ({ ...p, viewSettings: newVS }));
saveSetting('viewSettings', newVS);
}}
onSaveCustomReminder={(minutes: number) => {
const current: number[] = (profile.viewSettings as any)?.customReminderMinutes || [];
if (current.includes(minutes)) return;
const updated = [minutes, ...current].slice(0, 10);
const newVS = { ...(profile.viewSettings as any), customReminderMinutes: updated };
setProfile((p: any) => ({ ...p, viewSettings: newVS }));
saveSetting('viewSettings', newVS);
}}
onClose={() =>
setCalendarEventModal({ ...calendarEventModal, isOpen: false })
}
@ -11874,3 +11781,5 @@ function NotesSidebar({ task, onClose, updateTaskNotes, updateTaskUrl }: NotesSi
</>
);
}

File diff suppressed because one or more lines are too long