Compare commits
10 Commits
4a5910779e
...
4a18027068
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a18027068 | |||
| 4f140baa38 | |||
| 3cb59583ff | |||
| e7eb060643 | |||
| 530006060a | |||
| b9ed032f1f | |||
| 0a67745b85 | |||
| 5ebdf6d751 | |||
| 07401d3dfa | |||
| 0e189e53b3 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -43,4 +43,7 @@ prisma/dev.db-journal
|
|||||||
certificates
|
certificates
|
||||||
|
|
||||||
# Design reference files (not tracked)
|
# Design reference files (not tracked)
|
||||||
docs/design/
|
docs/design/
|
||||||
|
|
||||||
|
# Dokumentationen (nicht tracken)
|
||||||
|
APP_ERKLÄRUNG.md
|
||||||
255
docs/DESIGN_NOTES.md
Normal file
255
docs/DESIGN_NOTES.md
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
# 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), 7–8px (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: `120–180ms`, `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 | 4–10. 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 | 4–10. 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.*
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.111.3",
|
"version": "1.112.2",
|
||||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -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: 50;
|
||||||
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 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: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 {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, lazy, Suspense, useRef } from 'react';
|
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 } from 'lucide-react';
|
import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity, Check } from 'lucide-react';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { faServer } from '@fortawesome/free-solid-svg-icons';
|
import { faServer } from '@fortawesome/free-solid-svg-icons';
|
||||||
@ -27,7 +27,7 @@ function AnimatedDots() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reminder preset options (minutes)
|
// Reminder preset options (minutes)
|
||||||
const REMINDER_OPTIONS = [
|
const REMINDER_PRESETS = [
|
||||||
{ label: 'None', value: -1 },
|
{ label: 'None', value: -1 },
|
||||||
{ label: 'At time of event', value: 0 },
|
{ label: 'At time of event', value: 0 },
|
||||||
{ label: '5 minutes before', value: 5 },
|
{ label: '5 minutes before', value: 5 },
|
||||||
@ -40,6 +40,25 @@ const REMINDER_OPTIONS = [
|
|||||||
{ label: '1 week before', value: 10080 },
|
{ 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 = [
|
const BUSY_STATUS_OPTIONS = [
|
||||||
{ label: 'Busy', value: 'busy' },
|
{ label: 'Busy', value: 'busy' },
|
||||||
{ label: 'Free', value: 'free' },
|
{ label: 'Free', value: 'free' },
|
||||||
@ -63,6 +82,10 @@ interface CalendarEventModalProps {
|
|||||||
connections: any[];
|
connections: any[];
|
||||||
weekStartDay?: number; // 0=Sunday, 1=Monday
|
weekStartDay?: number; // 0=Sunday, 1=Monday
|
||||||
language?: string;
|
language?: string;
|
||||||
|
savedLocations?: string[];
|
||||||
|
onSaveLocation?: (loc: string) => void;
|
||||||
|
customReminderMinutes?: number[];
|
||||||
|
onSaveCustomReminder?: (minutes: number) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (eventData: any) => Promise<void>;
|
onSave: (eventData: any) => Promise<void>;
|
||||||
onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise<void>;
|
onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise<void>;
|
||||||
@ -76,6 +99,10 @@ export default function CalendarEventModal({
|
|||||||
connections,
|
connections,
|
||||||
weekStartDay = 0,
|
weekStartDay = 0,
|
||||||
language = 'en',
|
language = 'en',
|
||||||
|
savedLocations = [],
|
||||||
|
onSaveLocation,
|
||||||
|
customReminderMinutes = [],
|
||||||
|
onSaveCustomReminder,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
onDelete
|
onDelete
|
||||||
@ -167,6 +194,53 @@ export default function CalendarEventModal({
|
|||||||
const calendarSelectorRef = useRef<HTMLDivElement>(null);
|
const calendarSelectorRef = useRef<HTMLDivElement>(null);
|
||||||
const dialogRef = 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
|
// Focus trap
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@ -261,6 +335,11 @@ export default function CalendarEventModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save location to user profile if non-empty and new
|
||||||
|
if (location.trim() && onSaveLocation) {
|
||||||
|
onSaveLocation(location.trim());
|
||||||
|
}
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
@ -348,11 +427,29 @@ export default function CalendarEventModal({
|
|||||||
const updateReminder = (index: number, minutes: number) => {
|
const updateReminder = (index: number, minutes: number) => {
|
||||||
if (minutes === -1) {
|
if (minutes === -1) {
|
||||||
setReminders(reminders.filter((_, i) => i !== index));
|
setReminders(reminders.filter((_, i) => i !== index));
|
||||||
|
setCustomEditorIdx(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minutes === CUSTOM_REMINDER_SENTINEL) {
|
||||||
|
setCustomEditorIdx(index);
|
||||||
|
setCustomAmount(30);
|
||||||
|
setReminderUnit('minutes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updated = [...reminders];
|
const updated = [...reminders];
|
||||||
updated[index] = { ...updated[index], minutes };
|
updated[index] = { ...updated[index], minutes };
|
||||||
setReminders(updated);
|
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 = () => {
|
const addReminder = () => {
|
||||||
@ -687,24 +784,61 @@ export default function CalendarEventModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Location */}
|
{/* Location with autocomplete */}
|
||||||
<div style={iconRow}>
|
<div style={{ ...iconRow, position: 'relative' }} ref={locationRef}>
|
||||||
<div style={iconCol} aria-hidden="true"><MapPin size={14} /></div>
|
<div style={iconCol} aria-hidden="true"><MapPin size={14} /></div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={location}
|
value={location}
|
||||||
onChange={e => setLocation(e.target.value)}
|
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([]); }}
|
||||||
placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'}
|
placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'}
|
||||||
aria-label={language === 'de' ? 'Ort' : 'Location'}
|
aria-label={language === 'de' ? 'Ort' : 'Location'}
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded={locationSuggestions.length > 0}
|
||||||
style={{
|
style={{
|
||||||
...fieldCol, padding: '2px 0', border: 'none',
|
...fieldCol, padding: '2px 0', border: 'none',
|
||||||
background: 'transparent', outline: 'none', fontSize: '0.8rem',
|
background: 'transparent', outline: 'none', fontSize: '0.8rem',
|
||||||
color: 'var(--weekly-text)',
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Alert */}
|
{/* Alert / Reminders */}
|
||||||
<div style={iconRow}>
|
<div style={iconRow}>
|
||||||
<div style={iconCol} aria-hidden="true"><Bell size={14} /></div>
|
<div style={iconCol} aria-hidden="true"><Bell size={14} /></div>
|
||||||
<div style={{ ...fieldCol, display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
<div style={{ ...fieldCol, display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||||
@ -714,23 +848,79 @@ export default function CalendarEventModal({
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
reminders.map((reminder, idx) => (
|
reminders.map((reminder, idx) => (
|
||||||
<div key={idx} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<div key={idx} style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||||||
<select
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
value={reminder.minutes}
|
<select
|
||||||
onChange={e => updateReminder(idx, parseInt(e.target.value))}
|
value={customEditorIdx === idx ? CUSTOM_REMINDER_SENTINEL : reminder.minutes}
|
||||||
aria-label={`${language === 'de' ? 'Erinnerung' : 'Reminder'} ${idx + 1}`}
|
onChange={e => updateReminder(idx, parseInt(e.target.value))}
|
||||||
style={{ ...inlineSelect, flex: 1 }}
|
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>
|
{buildReminderOptions().map(opt => (
|
||||||
))}
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
</select>
|
))}
|
||||||
<button
|
</select>
|
||||||
onClick={() => setReminders(reminders.filter((_, i) => i !== idx))}
|
<button
|
||||||
aria-label={language === 'de' ? `Erinnerung ${idx + 1} entfernen` : `Remove reminder ${idx + 1}`}
|
onClick={() => { setReminders(reminders.filter((_, i) => i !== idx)); if (customEditorIdx === idx) setCustomEditorIdx(null); }}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--weekly-text-light)', opacity: 0.5 }}>
|
aria-label={language === 'de' ? `Erinnerung ${idx + 1} entfernen` : `Remove reminder ${idx + 1}`}
|
||||||
<X size={12} aria-hidden="true" />
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--weekly-text-light)', opacity: 0.5 }}>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -299,7 +299,7 @@ export function GridTaskBlock({
|
|||||||
if (editingTaskId !== task.id) toggleTask(task.id);
|
if (editingTaskId !== task.id) toggleTask(task.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "flex-start", gap: "0.25rem", width: "100%", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "0.25rem", width: "100%", justifyContent: "space-between" }}>
|
||||||
{editingTaskId === task.id ? (
|
{editingTaskId === task.id ? (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@ -329,7 +329,7 @@ export function GridTaskBlock({
|
|||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "flex-start",
|
alignItems: "center",
|
||||||
gap: "3px",
|
gap: "3px",
|
||||||
overflow: "visible",
|
overflow: "visible",
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
@ -342,7 +342,7 @@ export function GridTaskBlock({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(showPriorityIcons || showProjectIcons) && (
|
{(showPriorityIcons || showProjectIcons) && (
|
||||||
<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 }}>
|
<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 }}>
|
||||||
{showPriorityIcons && (() => {
|
{showPriorityIcons && (() => {
|
||||||
const pb = getPriorityBadge(task, priorityStyle, 11);
|
const pb = getPriorityBadge(task, priorityStyle, 11);
|
||||||
return pb ? (
|
return pb ? (
|
||||||
@ -380,7 +380,7 @@ export function GridTaskBlock({
|
|||||||
onChange={(e) => { e.stopPropagation(); toggleTask(task.id); }}
|
onChange={(e) => { e.stopPropagation(); toggleTask(task.id); }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="task-checkbox flex-shrink-0"
|
className="task-checkbox flex-shrink-0"
|
||||||
style={{ width: "12px", height: "12px", margin: 0, cursor: "pointer", position: "relative", top: "3px", left: "-2px", accentColor: "#FFF" }}
|
style={{ width: "12px", height: "12px", margin: 0, cursor: "pointer", flexShrink: 0, 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" }}>
|
<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" },
|
synology: { icon: faServer, color: "#007AFF", label: "Synology" },
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<span style={{ display: "flex", alignItems: "flex-start", gap: "3px", flexShrink: 0, paddingTop: "2px" }}>
|
<span style={{ display: "flex", alignItems: "center", gap: "3px", flexShrink: 0 }}>
|
||||||
{rolling && (
|
{rolling && (
|
||||||
<span title="Auto-rolling task" style={{ display: "inline-flex", alignItems: "center", color: "var(--weekly-teal, #009a9a)" }}>
|
<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">
|
<svg viewBox="0 0 24 24" width="10" height="10" stroke="currentColor" strokeWidth="2.5" fill="none" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
@ -1257,6 +1261,8 @@ export default function WeeklyView() {
|
|||||||
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
const [isRecurringTasksOpen, setIsRecurringTasksOpen] = useState(false);
|
||||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||||
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
const datePickerBtnRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [showNavDatePicker, setShowNavDatePicker] = useState(false);
|
||||||
|
const navDatePickerBtnRef = useRef<HTMLButtonElement>(null);
|
||||||
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
||||||
const [leftRailExpanded, setLeftRailExpanded] = useState(false);
|
const [leftRailExpanded, setLeftRailExpanded] = useState(false);
|
||||||
const [flyoutSection, setFlyoutSection] = useState<string | null>(null);
|
const [flyoutSection, setFlyoutSection] = useState<string | null>(null);
|
||||||
@ -1400,6 +1406,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 +1423,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 +2415,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));
|
||||||
@ -3159,6 +3187,9 @@ export default function WeeklyView() {
|
|||||||
? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime }
|
? { ...ev, startTime: eventDragState.originalStartTime, endTime: eventDragState.originalEndTime }
|
||||||
: ev
|
: ev
|
||||||
));
|
));
|
||||||
|
} else {
|
||||||
|
const connId = getConnectionIdForCalendar(eventDragState.calendarId);
|
||||||
|
fetchCalendarEvents(true, connId);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setRawCalendarEvents(prev => prev.map(ev =>
|
setRawCalendarEvents(prev => prev.map(ev =>
|
||||||
@ -3178,7 +3209,7 @@ export default function WeeklyView() {
|
|||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [eventDragState, effectiveCellDuration, calendarEvents]);
|
}, [eventDragState, effectiveCellDuration, calendarEvents, fetchCalendarEvents, getConnectionIdForCalendar]);
|
||||||
|
|
||||||
// Slot drag-to-create: mousemove + mouseup on document (always active, ref-gated)
|
// Slot drag-to-create: mousemove + mouseup on document (always active, ref-gated)
|
||||||
const effectiveCellDurationRef = useRef(effectiveCellDuration);
|
const effectiveCellDurationRef = useRef(effectiveCellDuration);
|
||||||
@ -3428,6 +3459,9 @@ export default function WeeklyView() {
|
|||||||
setRawCalendarEvents(prev => prev.map(ev =>
|
setRawCalendarEvents(prev => prev.map(ev =>
|
||||||
ev.id === eventId ? { ...ev, startTime: originalStartTime, endTime: originalEndTime } : ev
|
ev.id === eventId ? { ...ev, startTime: originalStartTime, endTime: originalEndTime } : ev
|
||||||
));
|
));
|
||||||
|
} else {
|
||||||
|
const connId = getConnectionIdForCalendar(calendarId);
|
||||||
|
fetchCalendarEvents(true, connId);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setRawCalendarEvents(prev => prev.map(ev =>
|
setRawCalendarEvents(prev => prev.map(ev =>
|
||||||
@ -5383,6 +5417,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 +5499,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 +5530,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 +5559,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 +5573,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 +5868,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 +5940,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}
|
||||||
@ -5932,8 +5970,9 @@ export default function WeeklyView() {
|
|||||||
{railSep}
|
{railSep}
|
||||||
{/* History flyout (undo / redo) */}
|
{/* 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>
|
<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 */}
|
{/* Refresh + Print */}
|
||||||
<button onClick={() => { fetchCalendarEvents(true); fetchTasks(); }} style={{ ...railIconBtnStyle }} title="Refresh"><RefreshCcw size={15} /></button>
|
<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 }} />
|
<div style={{ flex: 1 }} />
|
||||||
{railSep}
|
{railSep}
|
||||||
{/* Settings (cogwheel) + User menu at bottom */}
|
{/* Settings (cogwheel) + User menu at bottom */}
|
||||||
@ -6242,7 +6281,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) */}
|
||||||
@ -6338,6 +6377,15 @@ export default function WeeklyView() {
|
|||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */}
|
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */}
|
||||||
@ -6502,27 +6550,50 @@ 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 */}
|
||||||
<div className="header-desktop-only flex items-center gap-1.5">
|
{!useLeftRail && (
|
||||||
<button onClick={handleUndo} disabled={undoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Undo (Ctrl+Z)">
|
<div className="header-desktop-only flex items-center gap-1.5">
|
||||||
<Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
|
<button onClick={handleUndo} disabled={undoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Undo (Ctrl+Z)">
|
||||||
</button>
|
<Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
|
||||||
<button onClick={handleRedo} disabled={redoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Redo (Ctrl+Y)">
|
</button>
|
||||||
<Redo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
|
<button onClick={handleRedo} disabled={redoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Redo (Ctrl+Y)">
|
||||||
</button>
|
<Redo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
|
||||||
<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">
|
</button>
|
||||||
<Plus size={17} className="text-gray-600 hover:text-black transition-colors" />
|
<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">
|
||||||
</button>
|
<Plus size={17} className="text-gray-600 hover:text-black transition-colors" />
|
||||||
<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"}>
|
</button>
|
||||||
{profile.showNextTask ? <Play size={17} className="text-teal-600" /> : <Target size={17} className="text-gray-400" />}
|
<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"}>
|
||||||
</button>
|
{profile.showNextTask ? <Play size={17} className="text-teal-600" /> : <Target size={17} className="text-gray-400" />}
|
||||||
<button onClick={() => setShowFocusMode(true)} className="weekly-btn-icon" title="Enter Focus Mode">
|
</button>
|
||||||
<Zap size={17} className="text-gray-600 hover:text-yellow-500 transition-colors" />
|
<button onClick={() => setShowFocusMode(true)} className="weekly-btn-icon" title="Enter Focus Mode">
|
||||||
</button>
|
<Zap size={17} className="text-gray-600 hover:text-yellow-500 transition-colors" />
|
||||||
<button onClick={() => setDarkMode(!darkMode)} className="weekly-btn-icon" title={darkMode ? "Light Mode" : "Dark Mode"}>
|
</button>
|
||||||
{darkMode ? <Sun size={17} className="text-yellow-500" /> : <Moon size={17} className="text-gray-500" />}
|
<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} />
|
||||||
</button>
|
</button>
|
||||||
|
{showNavDatePicker && !isMobile && (
|
||||||
|
<SimpleDatePicker
|
||||||
|
selected={currentWeekStart}
|
||||||
|
onSelect={(date) => { setCurrentWeekStart(getStartOfWeek(date)); setShowNavDatePicker(false); }}
|
||||||
|
onClose={() => setShowNavDatePicker(false)}
|
||||||
|
language={profile.language}
|
||||||
|
anchorRef={navDatePickerBtnRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation Controls — always visible */}
|
{/* Navigation Controls — always visible */}
|
||||||
@ -6539,7 +6610,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>
|
||||||
@ -6588,9 +6659,13 @@ export default function WeeklyView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop only: Recurring Tasks + New Project */}
|
{/* Desktop only: Recurring Tasks + New Project */}
|
||||||
<button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button>
|
{!useLeftRail && (
|
||||||
<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 */}
|
{/* User Menu */}
|
||||||
<UserMenu
|
<UserMenu
|
||||||
@ -8030,7 +8105,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)}
|
||||||
>
|
>
|
||||||
@ -9214,6 +9289,24 @@ export default function WeeklyView() {
|
|||||||
connections={connections}
|
connections={connections}
|
||||||
weekStartDay={profile.weekStartDay ?? 1}
|
weekStartDay={profile.weekStartDay ?? 1}
|
||||||
language={profile.language}
|
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={() =>
|
onClose={() =>
|
||||||
setCalendarEventModal({ ...calendarEventModal, isOpen: false })
|
setCalendarEventModal({ ...calendarEventModal, isOpen: false })
|
||||||
}
|
}
|
||||||
@ -11781,5 +11874,3 @@ function NotesSidebar({ task, onClose, updateTaskNotes, updateTaskUrl }: NotesSi
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user