diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e314f0..5cfcf54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,21 @@
All notable changes to this project are documented in this file.
+## [1.119.2] - 2026-09-07
+
+### Changed
+- Die restlichen fünf Fremdkomponenten aus `WeeklyView.tsx` liegen jetzt unter
+ `src/components/weekly/`: `TaskInput`, `SomedayAddTask`, `TaskItem` (mit über 900 Zeilen
+ die grösste), `ListVisualsPopover` und `TabVisualsPopover`.
+- Die Prioritäts-Abzeichen sind nach `weekly/priority-badge.tsx` gewandert. `GridTaskBlock`
+ importierte `getPriorityBadge` aus `WeeklyView.tsx` und zog sich damit die gesamte
+ Wochenansicht in seinen Abhängigkeitsgraphen; `WeeklyView` re-exportiert die Funktion,
+ bestehende Importe bleiben gültig.
+- **`WeeklyView.tsx`: 11.148 → 9.711 Zeilen** — erstmals unter 10.000. Gegenüber dem Stand
+ vor dem Audit (11.906) sind das 2.195 Zeilen oder 18 Prozent.
+
+Verifiziert: Typecheck 0 Fehler, `npm run build` Exit 0, 63 Unit-Tests grün.
+
## [1.119.1] - 2026-09-07
### Changed
diff --git a/TODO.md b/TODO.md
index 7515ff2..24adcb4 100644
--- a/TODO.md
+++ b/TODO.md
@@ -136,7 +136,7 @@ Betroffen sind genau zwei Dateien:
| Datei | Zeilen |
|---|---:|
-| `src/components/WeeklyView.tsx` | 11.148 *(war 11.906)* |
+| `src/components/WeeklyView.tsx` | **9.711** *(war 11.906)* |
| `src/components/SettingsSidebar.tsx` | 5.445 |
**Voraussetzung:** C1 muss laufen — ohne Typecheck ist ein Refactoring dieser Größe
@@ -148,8 +148,15 @@ Blindflug. Ebenso hilfreich: A2, damit es überhaupt Tests gibt.
- [x] **Entdeckung:** In `WeeklyView.tsx` steckten **acht eigenständige Komponenten**, die
gar nichts mit der Wochenansicht zu tun haben — zusammen rund 1.870 Zeilen. Drei davon
sind heraus: `ProjectIcon`, `ProjectsSidebar`, `NotesSidebar`.
-- [ ] Restliche fünf herauslösen: `TaskInput`, `SomedayAddTask`, `TaskItem`,
- `ListVisualsPopover`, `TabVisualsPopover`
+- [x] Restliche fünf herausgelöst: `TaskInput`, `SomedayAddTask`, `TaskItem`,
+ `ListVisualsPopover`, `TabVisualsPopover` — dazu die Prioritäts-Abzeichen, die
+ `GridTaskBlock` von `WeeklyView` importierte
+- [ ] Someday-Listen aus dem Haupt-JSX herauslösen (jetzt echtes Props-Design nötig)
+- [ ] All-Day-Leiste herauslösen
+- [ ] Zeitraster und Tagesspalten herauslösen
+- [ ] Einstellungs-Flyouts herauslösen
+- [ ] Drag & Drop in einen Hook (~15 Abhängigkeiten — erst sinnvoll, wenn der State
+ geordneter ist; ein Hook mit 15 Parametern wäre keine Verbesserung)
- [x] Datums-/Zeitraster-Helfer nach `src/lib/weekly/date-utils.ts` (13 Funktionen, 202 Zeilen)
- [x] Cookie-/Geräteeinstellungen nach `src/lib/weekly/device-settings.ts`
- [x] Formatierungs-Helfer nach `src/lib/weekly/format.ts`
@@ -158,11 +165,6 @@ Blindflug. Ebenso hilfreich: A2, damit es überhaupt Tests gibt.
vorher die drei Implementierungen auf Gleichheit prüfen
- [ ] `calendar-cache.ts` hat mit `getWeekStart` eine **vierte** eigene Wochenlogik —
ebenfalls gegen `date-utils.ts` ersetzen
-- [ ] Drag & Drop in einen Hook `useTaskDragDrop.ts`
-- [ ] Someday-Listen in `SomedayLists.tsx`
-- [ ] All-Day-Leiste in `AllDayBar.tsx`
-- [ ] Zeitraster + Tagesspalten in `TimeGrid.tsx` / `DayColumn.tsx`
-- [ ] Einstellungs-Flyouts in `QuickSettingsPanel.tsx`
- [ ] Daten-Laden in `useWeeklyData.ts`
- [ ] `WeeklyView.tsx` bleibt als Orchestrator übrig
diff --git a/package-lock.json b/package-lock.json
index 159a583..e1232af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "my-weekly-todo-list",
- "version": "1.119.1",
+ "version": "1.119.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-weekly-todo-list",
- "version": "1.119.1",
+ "version": "1.119.2",
"license": "MIT",
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
diff --git a/package.json b/package.json
index aaca392..4ad89eb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "my-weekly-todo-list",
- "version": "1.119.1",
+ "version": "1.119.2",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js",
"scripts": {
diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx
index 7d35f10..f1373d2 100644
--- a/src/components/WeeklyView.tsx
+++ b/src/components/WeeklyView.tsx
@@ -118,6 +118,13 @@ import WeekPrintModal from "./WeekPrintModal";
import { ProjectIcon } from "./weekly/ProjectIcon";
import { ProjectsSidebar } from "./weekly/ProjectsSidebar";
import { NotesSidebar } from "./weekly/NotesSidebar";
+import { TaskInput } from "./weekly/TaskInput";
+import { SomedayAddTask } from "./weekly/SomedayAddTask";
+import { TaskItem } from "./weekly/TaskItem";
+import { ListVisualsPopover, TabVisualsPopover } from "./weekly/VisualsPopover";
+import { getPriorityBadge } from "./weekly/priority-badge";
+// Re-Export: GridTaskBlock importiert getPriorityBadge historisch von hier
+export { getPriorityBadge };
import type {
RecurringTaskException, ViewStyle, KanbanStage, Task,
CalendarEvent, SomedayList, CellDuration,
@@ -9685,1460 +9692,16 @@ export default function WeeklyView() {
);
}
-// Task Input Component
-interface TaskInputProps {
- onAddTask: (title: string) => void;
- onDragOver: (e: React.DragEvent) => void;
- onDrop: (e: React.DragEvent) => void;
-}
-function TaskInput({ onAddTask, onDragOver, onDrop }: TaskInputProps) {
- const [newTaskTitle, setNewTaskTitle] = useState("");
- const handleAddTask = () => {
- if (newTaskTitle.trim()) {
- onAddTask(newTaskTitle);
- setNewTaskTitle("");
- }
- };
- return (
-
-
- );
-}
-function SomedayAddTask({
- listId,
- onAdd,
- onCancel,
- slotIdx,
- darkMode,
- showProjectIcons,
- showTaskCheckboxes,
-}: {
- listId: string;
- onAdd: (title: string) => void;
- onCancel?: () => void;
- slotIdx?: number;
- darkMode?: boolean;
- showProjectIcons?: boolean;
- showTaskCheckboxes?: boolean;
-}) {
- const [title, setTitle] = useState("");
- const inputRef = useRef(null);
- const handleSubmit = () => {
- if (title.trim()) {
- onAdd(title.trim());
- setTitle("");
- } else if (onCancel) {
- onCancel();
- }
- };
- useEffect(() => {
- if (slotIdx !== undefined && inputRef.current) {
- inputRef.current.focus();
- }
- }, [slotIdx]);
- const isBottomAddInput = slotIdx === undefined;
- if (isBottomAddInput) {
- // Render as a flex row that mirrors the task item layout so the input
- // aligns exactly with the task text column (icon spacer + checkbox spacer + input).
- return (
-
-
-
- );
- }
- // Inline slot editor: transparent, no border
- return (
-
-
-
- );
-}
-// Task Item Component
-interface TaskItemProps {
- task: Task;
- isEditing: boolean;
- onToggle: () => void;
- onEdit: () => void;
- onUpdate: (title: string) => void;
- onDelete: () => void;
- onNotes: (notes: string) => void;
- onUrl?: (url: string) => void;
- onRollToggle: () => void;
- onRecurrence: () => void;
- onDragStart: (e: DragEvent, task: Task) => void;
- onDragEnd: () => void;
- variant?: "default" | "minimal";
- isSomeday?: boolean;
- onAddSubTask?: (parentId: string, title: string) => void;
- onToggleSubTask?: (subTaskId: string) => void;
- onDeleteSubTask?: (subTaskId: string) => void;
- onUpdateSubTask?: (subTaskId: string, title: string) => void;
- editingTaskId?: string | null;
- onSetEditingTaskId?: (id: string | null) => void;
- isSubTask?: boolean;
- showTaskCheckboxes?: boolean;
- showProjectIcons?: boolean;
- showPriorityIcons?: boolean;
- priorityStyle?: string;
- projects?: { id: string; name: string; icon?: string | null; color?: string | null }[];
- onProjectAssign?: (taskId: string, projectId: string | null) => void;
- kanbanStages?: KanbanStage[];
-}
-
-// ── Priority indicator helper ─────────────────────────────────────
-// Renders a small priority badge for a task. The visual style is selected
-// by the user's `priorityStyle` setting:
-// - "eisenhower" → quadrant icons + colors (urgency × importance)
-// - "abcde" → letter A–E in a colored circle
-// - "ivylee" → digit 1–6 in a colored circle (rank persisted in priority)
-// - "pareto" → star for the "vital few" (importance flag)
-// Returns null when the task has no priority data for the active style.
-type PriorityBadge = { node: React.ReactNode; label: string };
-
-const PRIORITY_NUMBER_COLORS: Record = {
- "1": "#ef4444", "2": "#f97316", "3": "#eab308",
- "4": "#22c55e", "5": "#3b82f6", "6": "#8b5cf6",
-};
-const PRIORITY_LETTER_COLORS: Record = {
- A: "#ef4444", B: "#3b82f6", C: "#eab308", D: "#f97316", E: "#9ca3af",
-};
-
-function makeBadgeCircle(text: string, color: string, size: number, label: string): PriorityBadge {
- return {
- label,
- node: (
- {text}
- ),
- };
-}
-
-export function getPriorityBadge(task: Task, style: string, size = 12): PriorityBadge | null {
- // Universal "important" star — shown for any task explicitly marked important
- // (e.g. Outlook To-Do tasks with the star flag) regardless of the active priority style.
- const importantStar = (): PriorityBadge => ({
- label: "Important",
- node: ,
- });
-
- if (style === "abcde") {
- if (task.priority && PRIORITY_LETTER_COLORS[task.priority]) {
- return makeBadgeCircle(task.priority, PRIORITY_LETTER_COLORS[task.priority], size + 2, task.priority);
- }
- if (task.importance === true) return importantStar();
- return null;
- }
- if (style === "ivylee") {
- if (task.priority && PRIORITY_NUMBER_COLORS[task.priority]) {
- return makeBadgeCircle(task.priority, PRIORITY_NUMBER_COLORS[task.priority], size + 2, `Ivy Lee #${task.priority}`);
- }
- if (task.importance === true) return importantStar();
- return null;
- }
- if (style === "pareto") {
- if (task.importance !== true && task.priority !== "A" && task.priority !== "B") return null;
- return {
- label: "Vital Few (80/20)",
- node: ,
- };
- }
- // eisenhower (default)
- if (task.urgency != null && task.importance != null) {
- if (task.urgency && task.importance)
- return { label: "Sofort erledigen", node: };
- if (!task.urgency && task.importance)
- return { label: "Planen", node: };
- if (task.urgency && !task.importance)
- return { label: "Delegieren", node: };
- return { label: "Eliminieren", node: };
- }
- // Eisenhower with only importance set (e.g. imported from Outlook with no urgency)
- if (task.importance === true) return importantStar();
- return null;
-}
-
-function TaskItem({
- task,
- isEditing,
- onToggle,
- onEdit,
- onUpdate,
- onDelete,
- onNotes,
- onUrl,
- onRollToggle,
- onRecurrence,
- onDragStart,
- onDragEnd,
- variant = "default",
- isSomeday = false,
- onAddSubTask,
- onToggleSubTask,
- onDeleteSubTask,
- onUpdateSubTask,
- editingTaskId,
- onSetEditingTaskId,
- isSubTask = false,
- showTaskCheckboxes = false,
- showProjectIcons = false,
- showPriorityIcons = true,
- priorityStyle = "eisenhower",
- projects = [],
- onProjectAssign,
- kanbanStages = [],
-}: TaskItemProps) {
- const [editValue, setEditValue] = useState(task.title);
- const [isNotesOpen, setIsNotesOpen] = useState(false);
- const [notesValue, setNotesValue] = useState(task.markdownContent || "");
- const [isUrlOpen, setIsUrlOpen] = useState(false);
- const [urlValue, setUrlValue] = useState(task.url || "");
- const urlInputRef = useRef(null);
- const [isSubTaskInputOpen, setIsSubTaskInputOpen] = useState(false);
- const [isSubTasksOpen, setIsSubTasksOpen] = useState(false);
- const [newSubTaskTitle, setNewSubTaskTitle] = useState("");
- const [showProjectPicker, setShowProjectPicker] = useState(false);
- const projectPickerRef = useRef(null);
- const inputRef = useRef(null);
- const subTaskInputRef = useRef(null);
-
- // Touch: tap-to-reveal actions
- const [touchActive, setTouchActive] = useState(false);
- const taskItemRef = useRef(null);
-
- // Touch: swipe gesture state
- const [swipeX, setSwipeX] = useState(0);
- const swipeTouchStart = useRef({ x: 0, y: 0, swiping: false });
-
- // Close touch-active on outside click
- useEffect(() => {
- if (!touchActive) return;
- const handler = (e: Event) => {
- if (taskItemRef.current && !taskItemRef.current.contains(e.target as Node)) {
- setTouchActive(false);
- }
- };
- document.addEventListener("touchstart", handler);
- document.addEventListener("mousedown", handler);
- return () => {
- document.removeEventListener("touchstart", handler);
- document.removeEventListener("mousedown", handler);
- };
- }, [touchActive]);
-
- // Close project picker on outside click
- useEffect(() => {
- if (!showProjectPicker) return;
- const handler = (e: Event) => {
- if (projectPickerRef.current && !projectPickerRef.current.contains(e.target as Node)) {
- setShowProjectPicker(false);
- }
- };
- document.addEventListener("mousedown", handler);
- return () => document.removeEventListener("mousedown", handler);
- }, [showProjectPicker]);
-
- const needsSync = task.externalProvider && (
- !task.externalId ||
- !task.lastSyncedAt ||
- new Date(task.updatedAt) > new Date(task.lastSyncedAt)
- );
-
- useEffect(() => {
- if (isEditing && inputRef.current) {
- inputRef.current.focus();
- inputRef.current.select();
- }
- }, [isEditing]);
-
- // Focus URL input when opened; sync when task.url changes
- useEffect(() => { if (isUrlOpen) setTimeout(() => urlInputRef.current?.focus(), 50); }, [isUrlOpen]);
- useEffect(() => { setUrlValue(task.url || ""); }, [task.url]);
-
- const handleUrlSave = (val: string) => {
- setIsUrlOpen(false);
- if (val.trim() !== (task.url || "") && onUrl) {
- onUrl(val.trim());
- }
- };
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- onUpdate(editValue);
- };
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- (e.target as HTMLElement).blur();
- }
- if (e.key === "Escape") {
- setEditValue(task.title);
- onUpdate(task.title);
- }
- };
-
- return (
- {
- const stageColor = task.kanbanStage ? kanbanStages.find(s => s.id === task.kanbanStage)?.color : null;
- if (stageColor) return { borderLeft: `4px solid ${stageColor}`, paddingLeft: "6px" };
- if (task.project?.color) return { borderLeft: `3px solid ${task.project.color}`, paddingLeft: "6px" };
- return undefined;
- })()}
- draggable={!isEditing && !isNotesOpen && swipeX === 0}
- onDragStart={(e) => {
- // If dragging a subtask, don't drag the parent
- const target = e.target as HTMLElement;
- if (target.closest('.subtask-list')) {
- e.stopPropagation();
- return;
- }
- onDragStart(e as unknown as DragEvent, task);
- }}
- onDragEnd={onDragEnd}
- onClick={(e) => {
- // Touch: toggle action toolbar on tap
- if (window.matchMedia("(pointer: coarse)").matches && !isEditing) {
- const target = e.target as HTMLElement;
- if (target.closest(".task-actions") || target.closest("button")) return;
- setTouchActive(!touchActive);
- return;
- }
- if ((variant === "minimal" || isSomeday) && !isEditing) {
- const target = e.target as HTMLElement;
- if (
- target.tagName === "BUTTON" ||
- target.tagName === "INPUT" ||
- target.closest("button")
- )
- return;
- onEdit();
- }
- }}
- onTouchStart={() => {
- // No task-level swipe — container handles day navigation
- }}
- >
- {/* Swipe indicators */}
- {swipeX > 20 && (
-
- )}
- {swipeX < -20 && (
-
-
-
- )}
-
- {/* Visual Indicator for Rolling Tasks */}
- {task.isRolling && !task.completed && !isSomeday && (
-
- )}
-
-
- {isEditing ? (
- <>
- {/* Someday: match icon + checkbox column widths so textarea aligns with task text */}
- {isSomeday && showProjectIcons &&
}
- {isSomeday && showTaskCheckboxes &&
}
-
- >
- ) : (
- <>
- {/* Someday: fixed-width icon column — project icon, priority badge, delegation, or empty placeholder */}
- {isSomeday && (
-
- {showProjectIcons && task.project
- ?
- : (() => {
- if (showPriorityIcons) { const pb = getPriorityBadge(task, priorityStyle, 11); if (pb) return pb.node; }
- if (task.delegatedTo) return task.delegatedTo === "AI" ? : ;
- return null;
- })()
- }
-
- )}
- {/* Someday: checkbox outside text span so it aligns in its own column */}
- {isSomeday && showTaskCheckboxes && (
-
{ e.stopPropagation(); onToggle(); }}
- onClick={(e) => e.stopPropagation()}
- className="task-checkbox flex-shrink-0"
- style={{ width: "16px", height: "16px", margin: 0, cursor: "pointer", flexShrink: 0, accentColor: "var(--weekly-teal, #009a9a)", WebkitAppearance: "checkbox" }}
- />
- )}
-
{
- if (variant === "default" && !showTaskCheckboxes) onToggle();
- // For minimal/someday, parent onClick handles edit
- }}
- onDoubleClick={variant === "default" ? onEdit : undefined}
- style={
- variant === "minimal" || isSomeday
- ? { display: "flex", alignItems: "center", gap: "4px", ...(task.completed && showTaskCheckboxes ? { opacity: 0.5 } : {}) }
- : { display: "flex", alignItems: "center", gap: "6px", ...(task.completed && showTaskCheckboxes ? { opacity: 0.5 } : {}) }
- }
- >
- {/* Non-someday: checkbox stays inside text span */}
- {!isSomeday && showTaskCheckboxes && (
- { e.stopPropagation(); onToggle(); }}
- onClick={(e) => e.stopPropagation()}
- className="task-checkbox flex-shrink-0"
- style={{
- width: "16px",
- height: "16px",
- margin: 0,
- cursor: "pointer",
- position: "relative",
- top: "3px",
- left: "-2px",
- accentColor: "var(--weekly-teal, #009a9a)",
- WebkitAppearance: "checkbox"
- }}
- />
- )}
-
- {/* Non-someday only: priority/delegation icons inline. Someday shows them in the leading column. */}
- {!isSomeday && showPriorityIcons && (() => {
- const pb = getPriorityBadge(task, priorityStyle, 11);
- return pb ? (
-
- {pb.node}
-
- ) : null;
- })()}
- {!isSomeday && task.delegatedTo && (
-
- {task.delegatedTo === "AI"
- ?
- : }
-
- )}
- {/* Non-someday: inline project icon; someday uses the column above */}
- {showProjectIcons && task.project && !isSomeday && (
-
-
-
- )}
- {task.title}
-
- {(() => {
- const provider = task.externalProvider
- || (task.externalId?.startsWith("synology::") ? "synology" : null);
- if (!provider) return null;
- const iconMap: Record = {
- google: { icon: faGoogle, color: "#4285F4", label: "Google" },
- outlook: { icon: faMicrosoft, color: "#0078D4", label: "Microsoft" },
- apple: { icon: faApple, color: "#555", label: "Apple" },
- synology: { icon: faServer, color: "#007AFF", label: "Synology" },
- notion: { icon: faNotion, color: "#000000", label: "Notion" },
- };
- const info = iconMap[provider];
- if (!info) return null;
- return (
-
-
-
- );
- })()}
-
-
- {/* Subtask indicator - toggles subtask list */}
- {task.subTasks && task.subTasks.length > 0 && !isSubTask && (() => {
- const completed = task.subTasks!.filter(s => s.completed).length;
- const total = task.subTasks!.length;
- const allDone = completed === total;
- const expanded = isSubTasksOpen || isSubTaskInputOpen;
- const pct = total > 0 ? (completed / total) * 100 : 0;
- return (
-
- );
- })()}
-
- {/* URL indicator */}
- {task.url && (
-
e.stopPropagation()}
- title={task.url}
- style={{
- display: "inline-flex", alignItems: "center",
- padding: "2px 5px", borderRadius: "4px",
- background: "rgba(37,99,235,0.10)", color: "#2563eb",
- textDecoration: "none", flexShrink: 0,
- }}
- >
-
-
- )}
- {/* Note indicator - toggles inline notes */}
- {task.markdownContent && task.markdownContent.trim().length > 0 && (
-
- )}
-
-
- {/* Complete */}
-
- {/* Edit */}
-
-
- {/* Add Sub-task */}
- {!isSubTask && onAddSubTask && (
-
- )}
-
- {/* Recurrence (hidden for someday tasks) */}
- {!isSomeday && (
-
- )}
-
- {/* Notes */}
-
-
- {/* Project Assignment */}
- {!isSubTask && projects.length > 0 && onProjectAssign && (
-
-
- {showProjectPicker && (
-
- {task.projectId && (
-
- )}
- {projects.map((p) => (
-
- ))}
-
- )}
-
- )}
-
- {/* Roll Toggle - Active State Colored (hidden for someday tasks) */}
- {!task.completed && !isSomeday && (
-
- )}
-
- {/* Link */}
-
-
- {/* Delete */}
-
-
-
- >
- )}
-
-
- {/* URL editor */}
- {isUrlOpen && (
-
e.stopPropagation()} style={{ padding: "6px 8px", display: "flex", alignItems: "center", gap: "6px" }}>
-
-
setUrlValue(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Enter") { e.preventDefault(); handleUrlSave(urlValue); }
- if (e.key === "Escape") { setIsUrlOpen(false); setUrlValue(task.url || ""); }
- }}
- onBlur={() => handleUrlSave(urlValue)}
- placeholder="https://..."
- style={{ flex: 1, fontSize: "0.78rem", border: "1px solid var(--weekly-border, #ddd)", borderRadius: "4px", padding: "3px 6px", background: "var(--weekly-bg, white)", color: "var(--weekly-text, #333)", outline: "none" }}
- />
- {urlValue && (
-
- )}
- {urlValue && (
-
e.stopPropagation()} title="Open link" style={{ color: "#2563eb", flexShrink: 0, display: "inline-flex" }}>
-
-
- )}
-
- )}
-
- {/* Inline Notes Editor */}
- {isNotesOpen && (
-
e.stopPropagation()} style={{ padding: "0" }}>
-
-
-
-
- { setNotesValue(html); onNotes(html); }}
- placeholder="Add notes..."
- minHeight="80px"
- />
-
-
- )}
-
- {/* Sub-tasks section */}
- {
- !isSubTask && (isSubTasksOpen || isSubTaskInputOpen) && task.subTasks && task.subTasks.length > 0 && (
-
- )
- }
-
- {/* Add sub-task input */}
- {
- !isSubTask && isSubTaskInputOpen && (
-
e.stopPropagation()}>
-
-
- )
- }
-
-
- );
-}
-
-// Punkt 7+8 — popover for editing a someday list's icon and color.
-function ListVisualsPopover({
- list,
- darkMode,
- language,
- onChange,
- onClose,
- anchorRect,
-}: {
- list: { id: string; title: string; color?: string | null; icon?: string | null };
- darkMode: boolean;
- language: string;
- anchorRect?: DOMRect | null;
- onChange: (updates: { color?: string | null; icon?: string | null }) => void;
- onClose: () => void;
-}) {
- const [iconPickerOpen, setIconPickerOpen] = useState(false);
- const containerRef = useRef(null);
- const onCloseRef = useRef(onClose);
- useEffect(() => { onCloseRef.current = onClose; }, [onClose]);
- useEffect(() => {
- const onDoc = (e: MouseEvent) => {
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) onCloseRef.current();
- };
- document.addEventListener("mousedown", onDoc);
- return () => document.removeEventListener("mousedown", onDoc);
- }, []);
- const de = language === "de";
- const top = anchorRect ? anchorRect.top + 32 : -9999;
- const left = anchorRect ? anchorRect.left + 4 : -9999;
- return (
- e.stopPropagation()}
- >
-
-
- {de ? "Listen-Stil" : "List style"}
-
-
-
-
-
{de ? "Icon" : "Icon"}
-
- {list.icon && (
-
- )}
-
- {iconPickerOpen && (
-
- { onChange({ icon: name }); setIconPickerOpen(false); }}
- darkMode={darkMode}
- />
-
- )}
-
- {de ? "Farbe" : "Color"}
- onChange({ color: e.target.value })}
- style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }}
- />
- {list.color && (
-
- )}
-
-
- );
-}
-
-// Punkt 7 — popover for editing a tab's icon and color (stored in viewSettings JSON).
-function TabVisualsPopover({
- tabName,
- visuals,
- darkMode,
- language,
- onChange,
- onRename,
- onDissolve,
- onClose,
- anchorRect,
-}: {
- tabName: string;
- visuals: { color?: string; icon?: string };
- darkMode: boolean;
- language: string;
- anchorRect?: DOMRect | null;
- onChange: (updates: { color?: string | null; icon?: string | null }) => void;
- onRename: (newName: string) => void;
- onDissolve: () => void;
- onClose: () => void;
-}) {
- const [iconPickerOpen, setIconPickerOpen] = useState(false);
- const [renameValue, setRenameValue] = useState(tabName);
- const containerRef = useRef(null);
- const onCloseRef = useRef(onClose);
- useEffect(() => { onCloseRef.current = onClose; }, [onClose]);
- useEffect(() => {
- const onDoc = (e: MouseEvent) => {
- if (containerRef.current && !containerRef.current.contains(e.target as Node)) onCloseRef.current();
- };
- document.addEventListener("mousedown", onDoc);
- return () => document.removeEventListener("mousedown", onDoc);
- }, []);
- const de = language === "de";
- const labelStyle = { fontSize: "0.75rem", color: darkMode ? "#9ca3af" : "#666", flex: 1 } as const;
- return (
- e.stopPropagation()}
- >
- {/* Header */}
-
-
- {de ? "Tab bearbeiten" : "Edit tab"}
-
-
-
- {/* Rename */}
-
- {de ? "Name" : "Name"}
- setRenameValue(e.target.value)}
- onBlur={() => { if (renameValue.trim() && renameValue.trim() !== tabName) onRename(renameValue.trim()); }}
- onKeyDown={(e) => { if (e.key === "Enter") { if (renameValue.trim() && renameValue.trim() !== tabName) onRename(renameValue.trim()); (e.target as HTMLInputElement).blur(); } }}
- style={{ flex: 1, fontSize: "0.8rem", border: `1px solid ${darkMode ? "#444" : "#e5e7eb"}`, borderRadius: "6px", padding: "4px 8px", background: darkMode ? "#2a2a3a" : "#f9f9f9", color: darkMode ? "#e5e7eb" : "#222", outline: "none" }}
- />
-
- {/* Icon */}
-
-
{de ? "Icon" : "Icon"}
-
- {visuals.icon && (
-
- )}
-
- {iconPickerOpen && (
-
- { onChange({ icon: name }); setIconPickerOpen(false); }}
- darkMode={darkMode}
- />
-
- )}
- {/* Color */}
-
- {de ? "Farbe" : "Color"}
- onChange({ color: e.target.value })}
- style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }}
- />
- {visuals.color && (
-
- )}
-
- {/* Dissolve / delete */}
-
-
-
-
- );
-}
// Projects Sidebar Component
diff --git a/src/components/weekly/SomedayAddTask.tsx b/src/components/weekly/SomedayAddTask.tsx
new file mode 100644
index 0000000..f3a601b
--- /dev/null
+++ b/src/components/weekly/SomedayAddTask.tsx
@@ -0,0 +1,134 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+
+/**
+ * Eingabezeile für die "Irgendwann"-Listen. Lag bis v1.119.2 in WeeklyView.tsx.
+ */
+
+export function SomedayAddTask({
+ listId,
+ onAdd,
+ onCancel,
+ slotIdx,
+ darkMode,
+ showProjectIcons,
+ showTaskCheckboxes,
+}: {
+ listId: string;
+ onAdd: (title: string) => void;
+ onCancel?: () => void;
+ slotIdx?: number;
+ darkMode?: boolean;
+ showProjectIcons?: boolean;
+ showTaskCheckboxes?: boolean;
+}) {
+ const [title, setTitle] = useState("");
+ const inputRef = useRef(null);
+
+ const handleSubmit = () => {
+ if (title.trim()) {
+ onAdd(title.trim());
+ setTitle("");
+ } else if (onCancel) {
+ onCancel();
+ }
+ };
+
+ useEffect(() => {
+ if (slotIdx !== undefined && inputRef.current) {
+ inputRef.current.focus();
+ }
+ }, [slotIdx]);
+
+ const isBottomAddInput = slotIdx === undefined;
+
+ if (isBottomAddInput) {
+ // Render as a flex row that mirrors the task item layout so the input
+ // aligns exactly with the task text column (icon spacer + checkbox spacer + input).
+ return (
+
+
+
+ );
+ }
+
+ // Inline slot editor: transparent, no border
+ return (
+
+
+
+ );
+}
diff --git a/src/components/weekly/TaskInput.tsx b/src/components/weekly/TaskInput.tsx
new file mode 100644
index 0000000..7c0154e
--- /dev/null
+++ b/src/components/weekly/TaskInput.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import React, { useState, DragEvent } from "react";
+
+/**
+ * Eingabezeile zum Anlegen einer Aufgabe. Lag bis v1.119.2 in WeeklyView.tsx.
+ */
+
+// Task Input Component
+interface TaskInputProps {
+ onAddTask: (title: string) => void;
+ onDragOver: (e: React.DragEvent) => void;
+ onDrop: (e: React.DragEvent) => void;
+}
+
+export function TaskInput({ onAddTask, onDragOver, onDrop }: TaskInputProps) {
+ const [newTaskTitle, setNewTaskTitle] = useState("");
+
+ const handleAddTask = () => {
+ if (newTaskTitle.trim()) {
+ onAddTask(newTaskTitle);
+ setNewTaskTitle("");
+ }
+ };
+
+ return (
+
+
+ );
+}
diff --git a/src/components/weekly/TaskItem.tsx b/src/components/weekly/TaskItem.tsx
new file mode 100644
index 0000000..bfa2978
--- /dev/null
+++ b/src/components/weekly/TaskItem.tsx
@@ -0,0 +1,973 @@
+"use client";
+
+import React, { useState, useEffect, useRef, DragEvent } from "react";
+import dynamic from "next/dynamic";
+import { Bot, ChevronDown, Circle, User, X } from "lucide-react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faApple, faGoogle, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons";
+import { faServer } from "@fortawesome/free-solid-svg-icons";
+import type { Task, SomedayList, KanbanStage } from "@/lib/weekly/types";
+import { getPriorityBadge } from "./priority-badge";
+import { ProjectIcon } from "./ProjectIcon";
+
+const RichTextEditor = dynamic(() => import("../RichTextEditor"), { ssr: false });
+
+/**
+ * Eine Aufgabe in der Listendarstellung. Mit über 900 Zeilen die grösste der acht
+ * Komponenten, die in WeeklyView.tsx steckten.
+ */
+
+// Task Item Component
+interface TaskItemProps {
+ task: Task;
+ isEditing: boolean;
+ onToggle: () => void;
+ onEdit: () => void;
+ onUpdate: (title: string) => void;
+ onDelete: () => void;
+ onNotes: (notes: string) => void;
+ onUrl?: (url: string) => void;
+ onRollToggle: () => void;
+ onRecurrence: () => void;
+ onDragStart: (e: DragEvent, task: Task) => void;
+ onDragEnd: () => void;
+ variant?: "default" | "minimal";
+ isSomeday?: boolean;
+ onAddSubTask?: (parentId: string, title: string) => void;
+ onToggleSubTask?: (subTaskId: string) => void;
+ onDeleteSubTask?: (subTaskId: string) => void;
+ onUpdateSubTask?: (subTaskId: string, title: string) => void;
+ editingTaskId?: string | null;
+ onSetEditingTaskId?: (id: string | null) => void;
+ isSubTask?: boolean;
+ showTaskCheckboxes?: boolean;
+ showProjectIcons?: boolean;
+ showPriorityIcons?: boolean;
+ priorityStyle?: string;
+ projects?: { id: string; name: string; icon?: string | null; color?: string | null }[];
+ onProjectAssign?: (taskId: string, projectId: string | null) => void;
+ kanbanStages?: KanbanStage[];
+}
+
+export function TaskItem({
+ task,
+ isEditing,
+ onToggle,
+ onEdit,
+ onUpdate,
+ onDelete,
+ onNotes,
+ onUrl,
+ onRollToggle,
+ onRecurrence,
+ onDragStart,
+ onDragEnd,
+ variant = "default",
+ isSomeday = false,
+ onAddSubTask,
+ onToggleSubTask,
+ onDeleteSubTask,
+ onUpdateSubTask,
+ editingTaskId,
+ onSetEditingTaskId,
+ isSubTask = false,
+ showTaskCheckboxes = false,
+ showProjectIcons = false,
+ showPriorityIcons = true,
+ priorityStyle = "eisenhower",
+ projects = [],
+ onProjectAssign,
+ kanbanStages = [],
+}: TaskItemProps) {
+ const [editValue, setEditValue] = useState(task.title);
+ const [isNotesOpen, setIsNotesOpen] = useState(false);
+ const [notesValue, setNotesValue] = useState(task.markdownContent || "");
+ const [isUrlOpen, setIsUrlOpen] = useState(false);
+ const [urlValue, setUrlValue] = useState(task.url || "");
+ const urlInputRef = useRef(null);
+ const [isSubTaskInputOpen, setIsSubTaskInputOpen] = useState(false);
+ const [isSubTasksOpen, setIsSubTasksOpen] = useState(false);
+ const [newSubTaskTitle, setNewSubTaskTitle] = useState("");
+ const [showProjectPicker, setShowProjectPicker] = useState(false);
+ const projectPickerRef = useRef(null);
+ const inputRef = useRef(null);
+ const subTaskInputRef = useRef(null);
+
+ // Touch: tap-to-reveal actions
+ const [touchActive, setTouchActive] = useState(false);
+ const taskItemRef = useRef(null);
+
+ // Touch: swipe gesture state
+ const [swipeX, setSwipeX] = useState(0);
+ const swipeTouchStart = useRef({ x: 0, y: 0, swiping: false });
+
+ // Close touch-active on outside click
+ useEffect(() => {
+ if (!touchActive) return;
+ const handler = (e: Event) => {
+ if (taskItemRef.current && !taskItemRef.current.contains(e.target as Node)) {
+ setTouchActive(false);
+ }
+ };
+ document.addEventListener("touchstart", handler);
+ document.addEventListener("mousedown", handler);
+ return () => {
+ document.removeEventListener("touchstart", handler);
+ document.removeEventListener("mousedown", handler);
+ };
+ }, [touchActive]);
+
+ // Close project picker on outside click
+ useEffect(() => {
+ if (!showProjectPicker) return;
+ const handler = (e: Event) => {
+ if (projectPickerRef.current && !projectPickerRef.current.contains(e.target as Node)) {
+ setShowProjectPicker(false);
+ }
+ };
+ document.addEventListener("mousedown", handler);
+ return () => document.removeEventListener("mousedown", handler);
+ }, [showProjectPicker]);
+
+ const needsSync = task.externalProvider && (
+ !task.externalId ||
+ !task.lastSyncedAt ||
+ new Date(task.updatedAt) > new Date(task.lastSyncedAt)
+ );
+
+ useEffect(() => {
+ if (isEditing && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [isEditing]);
+
+ // Focus URL input when opened; sync when task.url changes
+ useEffect(() => { if (isUrlOpen) setTimeout(() => urlInputRef.current?.focus(), 50); }, [isUrlOpen]);
+ useEffect(() => { setUrlValue(task.url || ""); }, [task.url]);
+
+ const handleUrlSave = (val: string) => {
+ setIsUrlOpen(false);
+ if (val.trim() !== (task.url || "") && onUrl) {
+ onUrl(val.trim());
+ }
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ onUpdate(editValue);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ (e.target as HTMLElement).blur();
+ }
+ if (e.key === "Escape") {
+ setEditValue(task.title);
+ onUpdate(task.title);
+ }
+ };
+
+ return (
+ {
+ const stageColor = task.kanbanStage ? kanbanStages.find(s => s.id === task.kanbanStage)?.color : null;
+ if (stageColor) return { borderLeft: `4px solid ${stageColor}`, paddingLeft: "6px" };
+ if (task.project?.color) return { borderLeft: `3px solid ${task.project.color}`, paddingLeft: "6px" };
+ return undefined;
+ })()}
+ draggable={!isEditing && !isNotesOpen && swipeX === 0}
+ onDragStart={(e) => {
+ // If dragging a subtask, don't drag the parent
+ const target = e.target as HTMLElement;
+ if (target.closest('.subtask-list')) {
+ e.stopPropagation();
+ return;
+ }
+ onDragStart(e as unknown as DragEvent, task);
+ }}
+ onDragEnd={onDragEnd}
+ onClick={(e) => {
+ // Touch: toggle action toolbar on tap
+ if (window.matchMedia("(pointer: coarse)").matches && !isEditing) {
+ const target = e.target as HTMLElement;
+ if (target.closest(".task-actions") || target.closest("button")) return;
+ setTouchActive(!touchActive);
+ return;
+ }
+ if ((variant === "minimal" || isSomeday) && !isEditing) {
+ const target = e.target as HTMLElement;
+ if (
+ target.tagName === "BUTTON" ||
+ target.tagName === "INPUT" ||
+ target.closest("button")
+ )
+ return;
+ onEdit();
+ }
+ }}
+ onTouchStart={() => {
+ // No task-level swipe — container handles day navigation
+ }}
+ >
+ {/* Swipe indicators */}
+ {swipeX > 20 && (
+
+ )}
+ {swipeX < -20 && (
+
+
+
+ )}
+
+ {/* Visual Indicator for Rolling Tasks */}
+ {task.isRolling && !task.completed && !isSomeday && (
+
+ )}
+
+
+ {isEditing ? (
+ <>
+ {/* Someday: match icon + checkbox column widths so textarea aligns with task text */}
+ {isSomeday && showProjectIcons &&
}
+ {isSomeday && showTaskCheckboxes &&
}
+
+ >
+ ) : (
+ <>
+ {/* Someday: fixed-width icon column — project icon, priority badge, delegation, or empty placeholder */}
+ {isSomeday && (
+
+ {showProjectIcons && task.project
+ ?
+ : (() => {
+ if (showPriorityIcons) { const pb = getPriorityBadge(task, priorityStyle, 11); if (pb) return pb.node; }
+ if (task.delegatedTo) return task.delegatedTo === "AI" ? : ;
+ return null;
+ })()
+ }
+
+ )}
+ {/* Someday: checkbox outside text span so it aligns in its own column */}
+ {isSomeday && showTaskCheckboxes && (
+
{ e.stopPropagation(); onToggle(); }}
+ onClick={(e) => e.stopPropagation()}
+ className="task-checkbox flex-shrink-0"
+ style={{ width: "16px", height: "16px", margin: 0, cursor: "pointer", flexShrink: 0, accentColor: "var(--weekly-teal, #009a9a)", WebkitAppearance: "checkbox" }}
+ />
+ )}
+
{
+ if (variant === "default" && !showTaskCheckboxes) onToggle();
+ // For minimal/someday, parent onClick handles edit
+ }}
+ onDoubleClick={variant === "default" ? onEdit : undefined}
+ style={
+ variant === "minimal" || isSomeday
+ ? { display: "flex", alignItems: "center", gap: "4px", ...(task.completed && showTaskCheckboxes ? { opacity: 0.5 } : {}) }
+ : { display: "flex", alignItems: "center", gap: "6px", ...(task.completed && showTaskCheckboxes ? { opacity: 0.5 } : {}) }
+ }
+ >
+ {/* Non-someday: checkbox stays inside text span */}
+ {!isSomeday && showTaskCheckboxes && (
+ { e.stopPropagation(); onToggle(); }}
+ onClick={(e) => e.stopPropagation()}
+ className="task-checkbox flex-shrink-0"
+ style={{
+ width: "16px",
+ height: "16px",
+ margin: 0,
+ cursor: "pointer",
+ position: "relative",
+ top: "3px",
+ left: "-2px",
+ accentColor: "var(--weekly-teal, #009a9a)",
+ WebkitAppearance: "checkbox"
+ }}
+ />
+ )}
+
+ {/* Non-someday only: priority/delegation icons inline. Someday shows them in the leading column. */}
+ {!isSomeday && showPriorityIcons && (() => {
+ const pb = getPriorityBadge(task, priorityStyle, 11);
+ return pb ? (
+
+ {pb.node}
+
+ ) : null;
+ })()}
+ {!isSomeday && task.delegatedTo && (
+
+ {task.delegatedTo === "AI"
+ ?
+ : }
+
+ )}
+ {/* Non-someday: inline project icon; someday uses the column above */}
+ {showProjectIcons && task.project && !isSomeday && (
+
+
+
+ )}
+ {task.title}
+
+ {(() => {
+ const provider = task.externalProvider
+ || (task.externalId?.startsWith("synology::") ? "synology" : null);
+ if (!provider) return null;
+ const iconMap: Record = {
+ google: { icon: faGoogle, color: "#4285F4", label: "Google" },
+ outlook: { icon: faMicrosoft, color: "#0078D4", label: "Microsoft" },
+ apple: { icon: faApple, color: "#555", label: "Apple" },
+ synology: { icon: faServer, color: "#007AFF", label: "Synology" },
+ notion: { icon: faNotion, color: "#000000", label: "Notion" },
+ };
+ const info = iconMap[provider];
+ if (!info) return null;
+ return (
+
+
+
+ );
+ })()}
+
+
+ {/* Subtask indicator - toggles subtask list */}
+ {task.subTasks && task.subTasks.length > 0 && !isSubTask && (() => {
+ const completed = task.subTasks!.filter(s => s.completed).length;
+ const total = task.subTasks!.length;
+ const allDone = completed === total;
+ const expanded = isSubTasksOpen || isSubTaskInputOpen;
+ const pct = total > 0 ? (completed / total) * 100 : 0;
+ return (
+
+ );
+ })()}
+
+ {/* URL indicator */}
+ {task.url && (
+
e.stopPropagation()}
+ title={task.url}
+ style={{
+ display: "inline-flex", alignItems: "center",
+ padding: "2px 5px", borderRadius: "4px",
+ background: "rgba(37,99,235,0.10)", color: "#2563eb",
+ textDecoration: "none", flexShrink: 0,
+ }}
+ >
+
+
+ )}
+ {/* Note indicator - toggles inline notes */}
+ {task.markdownContent && task.markdownContent.trim().length > 0 && (
+
+ )}
+
+
+ {/* Complete */}
+
+ {/* Edit */}
+
+
+ {/* Add Sub-task */}
+ {!isSubTask && onAddSubTask && (
+
+ )}
+
+ {/* Recurrence (hidden for someday tasks) */}
+ {!isSomeday && (
+
+ )}
+
+ {/* Notes */}
+
+
+ {/* Project Assignment */}
+ {!isSubTask && projects.length > 0 && onProjectAssign && (
+
+
+ {showProjectPicker && (
+
+ {task.projectId && (
+
+ )}
+ {projects.map((p) => (
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Roll Toggle - Active State Colored (hidden for someday tasks) */}
+ {!task.completed && !isSomeday && (
+
+ )}
+
+ {/* Link */}
+
+
+ {/* Delete */}
+
+
+
+ >
+ )}
+
+
+ {/* URL editor */}
+ {isUrlOpen && (
+
e.stopPropagation()} style={{ padding: "6px 8px", display: "flex", alignItems: "center", gap: "6px" }}>
+
+
setUrlValue(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") { e.preventDefault(); handleUrlSave(urlValue); }
+ if (e.key === "Escape") { setIsUrlOpen(false); setUrlValue(task.url || ""); }
+ }}
+ onBlur={() => handleUrlSave(urlValue)}
+ placeholder="https://..."
+ style={{ flex: 1, fontSize: "0.78rem", border: "1px solid var(--weekly-border, #ddd)", borderRadius: "4px", padding: "3px 6px", background: "var(--weekly-bg, white)", color: "var(--weekly-text, #333)", outline: "none" }}
+ />
+ {urlValue && (
+
+ )}
+ {urlValue && (
+
e.stopPropagation()} title="Open link" style={{ color: "#2563eb", flexShrink: 0, display: "inline-flex" }}>
+
+
+ )}
+
+ )}
+
+ {/* Inline Notes Editor */}
+ {isNotesOpen && (
+
e.stopPropagation()} style={{ padding: "0" }}>
+
+
+
+
+ { setNotesValue(html); onNotes(html); }}
+ placeholder="Add notes..."
+ minHeight="80px"
+ />
+
+
+ )}
+
+ {/* Sub-tasks section */}
+ {
+ !isSubTask && (isSubTasksOpen || isSubTaskInputOpen) && task.subTasks && task.subTasks.length > 0 && (
+
+ )
+ }
+
+ {/* Add sub-task input */}
+ {
+ !isSubTask && isSubTaskInputOpen && (
+
e.stopPropagation()}>
+
+
+ )
+ }
+
+
+ );
+}
diff --git a/src/components/weekly/VisualsPopover.tsx b/src/components/weekly/VisualsPopover.tsx
new file mode 100644
index 0000000..776f26d
--- /dev/null
+++ b/src/components/weekly/VisualsPopover.tsx
@@ -0,0 +1,251 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+import dynamic from "next/dynamic";
+import { X, Trash2 } from "lucide-react";
+import { ProjectIcon } from "./ProjectIcon";
+
+const IconPicker = dynamic(() => import("../IconPicker"), { ssr: false });
+
+/**
+ * Auswahl von Farbe und Symbol für Listen und Reiter der "Irgendwann"-Ansicht.
+ * Lagen bis v1.119.2 in WeeklyView.tsx.
+ */
+
+// Punkt 7+8 — popover for editing a someday list's icon and color.
+export function ListVisualsPopover({
+ list,
+ darkMode,
+ language,
+ onChange,
+ onClose,
+ anchorRect,
+}: {
+ list: { id: string; title: string; color?: string | null; icon?: string | null };
+ darkMode: boolean;
+ language: string;
+ anchorRect?: DOMRect | null;
+ onChange: (updates: { color?: string | null; icon?: string | null }) => void;
+ onClose: () => void;
+}) {
+ const [iconPickerOpen, setIconPickerOpen] = useState(false);
+ const containerRef = useRef(null);
+ const onCloseRef = useRef(onClose);
+ useEffect(() => { onCloseRef.current = onClose; }, [onClose]);
+ useEffect(() => {
+ const onDoc = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) onCloseRef.current();
+ };
+ document.addEventListener("mousedown", onDoc);
+ return () => document.removeEventListener("mousedown", onDoc);
+ }, []);
+ const de = language === "de";
+ const top = anchorRect ? anchorRect.top + 32 : -9999;
+ const left = anchorRect ? anchorRect.left + 4 : -9999;
+ return (
+ e.stopPropagation()}
+ >
+
+
+ {de ? "Listen-Stil" : "List style"}
+
+
+
+
+
{de ? "Icon" : "Icon"}
+
+ {list.icon && (
+
+ )}
+
+ {iconPickerOpen && (
+
+ { onChange({ icon: name }); setIconPickerOpen(false); }}
+ darkMode={darkMode}
+ />
+
+ )}
+
+ {de ? "Farbe" : "Color"}
+ onChange({ color: e.target.value })}
+ style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }}
+ />
+ {list.color && (
+
+ )}
+
+
+ );
+}
+
+// Punkt 7 — popover for editing a tab's icon and color (stored in viewSettings JSON).
+export function TabVisualsPopover({
+ tabName,
+ visuals,
+ darkMode,
+ language,
+ onChange,
+ onRename,
+ onDissolve,
+ onClose,
+ anchorRect,
+}: {
+ tabName: string;
+ visuals: { color?: string; icon?: string };
+ darkMode: boolean;
+ language: string;
+ anchorRect?: DOMRect | null;
+ onChange: (updates: { color?: string | null; icon?: string | null }) => void;
+ onRename: (newName: string) => void;
+ onDissolve: () => void;
+ onClose: () => void;
+}) {
+ const [iconPickerOpen, setIconPickerOpen] = useState(false);
+ const [renameValue, setRenameValue] = useState(tabName);
+ const containerRef = useRef(null);
+ const onCloseRef = useRef(onClose);
+ useEffect(() => { onCloseRef.current = onClose; }, [onClose]);
+ useEffect(() => {
+ const onDoc = (e: MouseEvent) => {
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) onCloseRef.current();
+ };
+ document.addEventListener("mousedown", onDoc);
+ return () => document.removeEventListener("mousedown", onDoc);
+ }, []);
+ const de = language === "de";
+ const labelStyle = { fontSize: "0.75rem", color: darkMode ? "#9ca3af" : "#666", flex: 1 } as const;
+ return (
+ e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+ {de ? "Tab bearbeiten" : "Edit tab"}
+
+
+
+ {/* Rename */}
+
+ {de ? "Name" : "Name"}
+ setRenameValue(e.target.value)}
+ onBlur={() => { if (renameValue.trim() && renameValue.trim() !== tabName) onRename(renameValue.trim()); }}
+ onKeyDown={(e) => { if (e.key === "Enter") { if (renameValue.trim() && renameValue.trim() !== tabName) onRename(renameValue.trim()); (e.target as HTMLInputElement).blur(); } }}
+ style={{ flex: 1, fontSize: "0.8rem", border: `1px solid ${darkMode ? "#444" : "#e5e7eb"}`, borderRadius: "6px", padding: "4px 8px", background: darkMode ? "#2a2a3a" : "#f9f9f9", color: darkMode ? "#e5e7eb" : "#222", outline: "none" }}
+ />
+
+ {/* Icon */}
+
+
{de ? "Icon" : "Icon"}
+
+ {visuals.icon && (
+
+ )}
+
+ {iconPickerOpen && (
+
+ { onChange({ icon: name }); setIconPickerOpen(false); }}
+ darkMode={darkMode}
+ />
+
+ )}
+ {/* Color */}
+
+ {de ? "Farbe" : "Color"}
+ onChange({ color: e.target.value })}
+ style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }}
+ />
+ {visuals.color && (
+
+ )}
+
+ {/* Dissolve / delete */}
+
+
+
+
+ );
+}
diff --git a/src/components/weekly/priority-badge.tsx b/src/components/weekly/priority-badge.tsx
new file mode 100644
index 0000000..d6b6847
--- /dev/null
+++ b/src/components/weekly/priority-badge.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import React from "react";
+import { Archive, CalendarClock, CornerUpRight, Star, Zap } from "lucide-react";
+import type { Task } from "@/lib/weekly/types";
+
+/**
+ * Prioritäts-Abzeichen für Aufgaben.
+ *
+ * Lag bis v1.119.2 in WeeklyView.tsx — obwohl GridTaskBlock `getPriorityBadge` von dort
+ * importierte und sich damit die gesamte Wochenansicht in seinen Abhängigkeitsgraphen zog.
+ */
+
+// ── Priority indicator helper ─────────────────────────────────────
+// Renders a small priority badge for a task. The visual style is selected
+// by the user's `priorityStyle` setting:
+// - "eisenhower" → quadrant icons + colors (urgency × importance)
+// - "abcde" → letter A–E in a colored circle
+// - "ivylee" → digit 1–6 in a colored circle (rank persisted in priority)
+// - "pareto" → star for the "vital few" (importance flag)
+// Returns null when the task has no priority data for the active style.
+type PriorityBadge = { node: React.ReactNode; label: string };
+
+const PRIORITY_NUMBER_COLORS: Record = {
+ "1": "#ef4444", "2": "#f97316", "3": "#eab308",
+ "4": "#22c55e", "5": "#3b82f6", "6": "#8b5cf6",
+};
+
+const PRIORITY_LETTER_COLORS: Record = {
+ A: "#ef4444", B: "#3b82f6", C: "#eab308", D: "#f97316", E: "#9ca3af",
+};
+
+function makeBadgeCircle(text: string, color: string, size: number, label: string): PriorityBadge {
+ return {
+ label,
+ node: (
+ {text}
+ ),
+ };
+}
+
+export function getPriorityBadge(task: Task, style: string, size = 12): PriorityBadge | null {
+ // Universal "important" star — shown for any task explicitly marked important
+ // (e.g. Outlook To-Do tasks with the star flag) regardless of the active priority style.
+ const importantStar = (): PriorityBadge => ({
+ label: "Important",
+ node: ,
+ });
+
+ if (style === "abcde") {
+ if (task.priority && PRIORITY_LETTER_COLORS[task.priority]) {
+ return makeBadgeCircle(task.priority, PRIORITY_LETTER_COLORS[task.priority], size + 2, task.priority);
+ }
+ if (task.importance === true) return importantStar();
+ return null;
+ }
+ if (style === "ivylee") {
+ if (task.priority && PRIORITY_NUMBER_COLORS[task.priority]) {
+ return makeBadgeCircle(task.priority, PRIORITY_NUMBER_COLORS[task.priority], size + 2, `Ivy Lee #${task.priority}`);
+ }
+ if (task.importance === true) return importantStar();
+ return null;
+ }
+ if (style === "pareto") {
+ if (task.importance !== true && task.priority !== "A" && task.priority !== "B") return null;
+ return {
+ label: "Vital Few (80/20)",
+ node: ,
+ };
+ }
+ // eisenhower (default)
+ if (task.urgency != null && task.importance != null) {
+ if (task.urgency && task.importance)
+ return { label: "Sofort erledigen", node: };
+ if (!task.urgency && task.importance)
+ return { label: "Planen", node: };
+ if (task.urgency && !task.importance)
+ return { label: "Delegieren", node: };
+ return { label: "Eliminieren", node: };
+ }
+ // Eisenhower with only importance set (e.g. imported from Outlook with no urgency)
+ if (task.importance === true) return importantStar();
+ return null;
+}