feat: projects tab, priority icons, list/tab visuals
Wave-1 user feedback (points 7–10): - Settings: new Projects tab with full CRUD (name, color, icon picker) - Priority Icons render in simple/calendar/list views with style-specific visuals (Eisenhower icons, ABCDE letters, Ivy Lee 1–6, Pareto star). Toggle + style selector in Settings; Ivy Lee ranks now persist to task.priority so cross-view badges stay consistent. - Someday Lists gain optional icon (left of title) and color (left border tint), edited via a pencil-popup with IconPicker + color picker. - Tabs gain optional icon and color, stored in user.viewSettings JSON and edited via the same popup pattern. Schema: SomedayList.color/icon, User.showPriorityIcons/priorityStyle. v1.99.0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ac17ca2635
commit
52d160ccea
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-weekly-todo-list",
|
"name": "my-weekly-todo-list",
|
||||||
"version": "1.97.2",
|
"version": "1.99.0",
|
||||||
"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": {
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
-- User: priority style + visibility toggle
|
||||||
|
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "showPriorityIcons" BOOLEAN NOT NULL DEFAULT true;
|
||||||
|
ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "priorityStyle" TEXT NOT NULL DEFAULT 'eisenhower';
|
||||||
|
|
||||||
|
-- SomedayList: per-list color and icon (used by Punkt 7 + 8)
|
||||||
|
ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "color" TEXT;
|
||||||
|
ALTER TABLE "SomedayList" ADD COLUMN IF NOT EXISTS "icon" TEXT;
|
||||||
@ -93,6 +93,8 @@ model User {
|
|||||||
yearFontWeight String? @default("700")
|
yearFontWeight String? @default("700")
|
||||||
showTaskCheckboxes Boolean @default(false)
|
showTaskCheckboxes Boolean @default(false)
|
||||||
showProjectIcons Boolean @default(false)
|
showProjectIcons Boolean @default(false)
|
||||||
|
showPriorityIcons Boolean @default(true)
|
||||||
|
priorityStyle String @default("eisenhower")
|
||||||
weekStartDay Int @default(1)
|
weekStartDay Int @default(1)
|
||||||
emailVerificationCode String?
|
emailVerificationCode String?
|
||||||
dayHeaderGap String? @default("0.75em")
|
dayHeaderGap String? @default("0.75em")
|
||||||
@ -226,6 +228,8 @@ model SomedayList {
|
|||||||
title String
|
title String
|
||||||
order Int @default(0)
|
order Int @default(0)
|
||||||
tab String?
|
tab String?
|
||||||
|
color String?
|
||||||
|
icon String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
externalId String?
|
externalId String?
|
||||||
|
|||||||
@ -209,8 +209,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Single Update (Title and/or Tab)
|
// Handle Single Update (Title, Tab, Color, Icon)
|
||||||
const { id, title, tab } = body;
|
const { id, title, tab, color, icon } = body;
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@ -234,6 +234,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
const data: Record<string, any> = {};
|
const data: Record<string, any> = {};
|
||||||
if (title !== undefined) data.title = title;
|
if (title !== undefined) data.title = title;
|
||||||
if (tab !== undefined) data.tab = tab;
|
if (tab !== undefined) data.tab = tab;
|
||||||
|
if (color !== undefined) data.color = color || null;
|
||||||
|
if (icon !== undefined) data.icon = icon || null;
|
||||||
|
|
||||||
const list = await prisma.somedayList.update({
|
const list = await prisma.somedayList.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export async function GET(request: NextRequest) {
|
|||||||
showSchedule: true,
|
showSchedule: true,
|
||||||
showTaskCheckboxes: true,
|
showTaskCheckboxes: true,
|
||||||
showProjectIcons: true,
|
showProjectIcons: true,
|
||||||
|
showPriorityIcons: true,
|
||||||
|
priorityStyle: true,
|
||||||
cellDuration: true,
|
cellDuration: true,
|
||||||
viewStyle: true,
|
viewStyle: true,
|
||||||
viewDays: true,
|
viewDays: true,
|
||||||
@ -150,7 +152,7 @@ export async function PATCH(request: NextRequest) {
|
|||||||
hourLabelFormat, showSubHourSlots, allDayPosition,
|
hourLabelFormat, showSubHourSlots, allDayPosition,
|
||||||
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
cwFontFamily, cwFontSize, cwFontWeight, cwColor,
|
||||||
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
yearFontFamily, yearFontSize, yearFontWeight, yearColor,
|
||||||
showTaskCheckboxes, showProjectIcons, dayHeaderGap,
|
showTaskCheckboxes, showProjectIcons, showPriorityIcons, priorityStyle, dayHeaderGap,
|
||||||
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, weekStartDay, quoteSourceUrls, quoteLanguages,
|
showCompletedTasks, showLines, startDayOffset, weekdayFormat, weekdayCase, customWeekdayNames, weekStartDay, quoteSourceUrls, quoteLanguages,
|
||||||
kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale,
|
kanbanStages, lightTheme, darkTheme, notificationsEnabled, mobileFontScale,
|
||||||
weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings,
|
weatherEnabled, weatherLat, weatherLon, weatherLocation, weatherRecentCities, viewSettings,
|
||||||
@ -178,6 +180,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
...(showSchedule !== undefined && { showSchedule }),
|
...(showSchedule !== undefined && { showSchedule }),
|
||||||
...(showTaskCheckboxes !== undefined && { showTaskCheckboxes }),
|
...(showTaskCheckboxes !== undefined && { showTaskCheckboxes }),
|
||||||
...(showProjectIcons !== undefined && { showProjectIcons }),
|
...(showProjectIcons !== undefined && { showProjectIcons }),
|
||||||
|
...(showPriorityIcons !== undefined && { showPriorityIcons }),
|
||||||
|
...(priorityStyle !== undefined && { priorityStyle }),
|
||||||
...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }),
|
...(cellDuration !== undefined && !isNaN(cellDuration) && { cellDuration }),
|
||||||
...(viewStyle !== undefined && { viewStyle }),
|
...(viewStyle !== undefined && { viewStyle }),
|
||||||
...(viewDays !== undefined && !isNaN(viewDays) && { viewDays }),
|
...(viewDays !== undefined && !isNaN(viewDays) && { viewDays }),
|
||||||
@ -283,6 +287,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
showSchedule: true,
|
showSchedule: true,
|
||||||
showTaskCheckboxes: true,
|
showTaskCheckboxes: true,
|
||||||
showProjectIcons: true,
|
showProjectIcons: true,
|
||||||
|
showPriorityIcons: true,
|
||||||
|
priorityStyle: true,
|
||||||
cellDuration: true,
|
cellDuration: true,
|
||||||
viewStyle: true,
|
viewStyle: true,
|
||||||
viewDays: true,
|
viewDays: true,
|
||||||
|
|||||||
@ -39,6 +39,8 @@ interface GridTaskBlockProps {
|
|||||||
workingHoursStart: number;
|
workingHoursStart: number;
|
||||||
showTaskCheckboxes?: boolean;
|
showTaskCheckboxes?: boolean;
|
||||||
showProjectIcons?: boolean;
|
showProjectIcons?: boolean;
|
||||||
|
showPriorityIcons?: boolean;
|
||||||
|
priorityStyle?: string;
|
||||||
projects?: any[];
|
projects?: any[];
|
||||||
onProjectAssign?: (taskId: string, projectId: string | null) => void;
|
onProjectAssign?: (taskId: string, projectId: string | null) => void;
|
||||||
kanbanStages?: KanbanStage[];
|
kanbanStages?: KanbanStage[];
|
||||||
@ -76,6 +78,8 @@ export function GridTaskBlock({
|
|||||||
workingHoursStart,
|
workingHoursStart,
|
||||||
showTaskCheckboxes,
|
showTaskCheckboxes,
|
||||||
showProjectIcons,
|
showProjectIcons,
|
||||||
|
showPriorityIcons = true,
|
||||||
|
priorityStyle = "eisenhower",
|
||||||
projects,
|
projects,
|
||||||
onProjectAssign,
|
onProjectAssign,
|
||||||
kanbanStages = [],
|
kanbanStages = [],
|
||||||
|
|||||||
@ -70,6 +70,8 @@ interface PriorityViewProps {
|
|||||||
darkMode: boolean;
|
darkMode: boolean;
|
||||||
language?: string;
|
language?: string;
|
||||||
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
onUpdateTask: (id: string, fields: Partial<PriorityTask>) => Promise<void>;
|
||||||
|
initialMethod?: PriorityMethod;
|
||||||
|
onMethodChange?: (m: PriorityMethod) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ABCDE_LABELS: Record<string, { label: string; desc: string; color: string; bg: string }> = {
|
const ABCDE_LABELS: Record<string, { label: string; desc: string; color: string; bg: string }> = {
|
||||||
@ -123,8 +125,14 @@ export default function PriorityView({
|
|||||||
darkMode,
|
darkMode,
|
||||||
language = "en",
|
language = "en",
|
||||||
onUpdateTask,
|
onUpdateTask,
|
||||||
|
initialMethod,
|
||||||
|
onMethodChange,
|
||||||
}: PriorityViewProps) {
|
}: PriorityViewProps) {
|
||||||
const [method, setMethod] = useState<PriorityMethod>("eisenhower");
|
const [method, setMethodState] = useState<PriorityMethod>(initialMethod || "eisenhower");
|
||||||
|
const setMethod = useCallback((m: PriorityMethod) => {
|
||||||
|
setMethodState(m);
|
||||||
|
onMethodChange?.(m);
|
||||||
|
}, [onMethodChange]);
|
||||||
const [filterProject, setFilterProject] = useState("");
|
const [filterProject, setFilterProject] = useState("");
|
||||||
const [filterList, setFilterList] = useState("");
|
const [filterList, setFilterList] = useState("");
|
||||||
const [filterTimespan, setFilterTimespan] = useState("all");
|
const [filterTimespan, setFilterTimespan] = useState("all");
|
||||||
@ -135,7 +143,15 @@ export default function PriorityView({
|
|||||||
const [delegateTo, setDelegateTo] = useState("");
|
const [delegateTo, setDelegateTo] = useState("");
|
||||||
const [delegateNote, setDelegateNote] = useState("");
|
const [delegateNote, setDelegateNote] = useState("");
|
||||||
const [delegateType, setDelegateType] = useState<"person" | "ai">("person");
|
const [delegateType, setDelegateType] = useState<"person" | "ai">("person");
|
||||||
const [ivyLeeSelected, setIvyLeeSelected] = useState<Set<string>>(new Set());
|
const [ivyLeeSelected, setIvyLeeSelected] = useState<Set<string>>(() => {
|
||||||
|
// Hydrate from any task that already has a numeric priority "1"-"6"
|
||||||
|
const init = new Set<string>();
|
||||||
|
const ranked = tasks
|
||||||
|
.filter((t) => t.priority && /^[1-6]$/.test(t.priority))
|
||||||
|
.sort((a, b) => Number(a.priority) - Number(b.priority));
|
||||||
|
for (const t of ranked) init.add(t.id);
|
||||||
|
return init;
|
||||||
|
});
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
@ -251,12 +267,21 @@ export default function PriorityView({
|
|||||||
const toggleIvyLee = useCallback((id: string) => {
|
const toggleIvyLee = useCallback((id: string) => {
|
||||||
setIvyLeeSelected((prev) => {
|
setIvyLeeSelected((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(id)) { next.delete(id); return next; }
|
const removing = next.has(id);
|
||||||
if (next.size >= 6) return prev; // max 6
|
if (removing) {
|
||||||
|
next.delete(id);
|
||||||
|
onUpdateTask(id, { priority: null });
|
||||||
|
} else {
|
||||||
|
if (next.size >= 6) return prev;
|
||||||
next.add(id);
|
next.add(id);
|
||||||
|
}
|
||||||
|
// Re-rank all selected tasks 1..N so cross-view badges stay consistent
|
||||||
|
Array.from(next).forEach((tid, idx) => {
|
||||||
|
onUpdateTask(tid, { priority: String(idx + 1) });
|
||||||
|
});
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, [onUpdateTask]);
|
||||||
|
|
||||||
const openDelegate = useCallback((task: PriorityTask) => {
|
const openDelegate = useCallback((task: PriorityTask) => {
|
||||||
setDelegateModal(task);
|
setDelegateModal(task);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { X, Type, Space, CheckSquare, Calendar, Minus } from "lucide-react";
|
import { X, Type, Space, CheckSquare, Calendar, Minus, Target } from "lucide-react";
|
||||||
|
|
||||||
interface QuickSettingsProps {
|
interface QuickSettingsProps {
|
||||||
fontSize: string;
|
fontSize: string;
|
||||||
@ -12,6 +12,8 @@ interface QuickSettingsProps {
|
|||||||
onStartDayOffsetChange: (offset: number) => void;
|
onStartDayOffsetChange: (offset: number) => void;
|
||||||
showLines: boolean;
|
showLines: boolean;
|
||||||
onShowLinesChange: (show: boolean) => void;
|
onShowLinesChange: (show: boolean) => void;
|
||||||
|
showPriorityIcons?: boolean;
|
||||||
|
onShowPriorityIconsChange?: (show: boolean) => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
darkMode?: boolean;
|
darkMode?: boolean;
|
||||||
@ -28,6 +30,8 @@ export default function QuickSettingsSidebar({
|
|||||||
onStartDayOffsetChange,
|
onStartDayOffsetChange,
|
||||||
showLines,
|
showLines,
|
||||||
onShowLinesChange,
|
onShowLinesChange,
|
||||||
|
showPriorityIcons,
|
||||||
|
onShowPriorityIconsChange,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
darkMode,
|
darkMode,
|
||||||
@ -198,6 +202,17 @@ export default function QuickSettingsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
<Toggle checked={showLines} onChange={onShowLinesChange} />
|
<Toggle checked={showLines} onChange={onShowLinesChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Show Priority Icons */}
|
||||||
|
{onShowPriorityIconsChange && (
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
||||||
|
<Target size={14} style={{ color: labelColor }} />
|
||||||
|
<span style={{ fontSize: "0.75rem", color: labelColor }}>Priority Icons</span>
|
||||||
|
</div>
|
||||||
|
<Toggle checked={showPriorityIcons ?? true} onChange={onShowPriorityIconsChange} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef, useMemo, useCallback } from "react"
|
|||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faApple, faGoogle, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons";
|
import { faApple, faGoogle, faMicrosoft, faNotion } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
import { faServer, faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
import CalendarSyncRulesPanel from "./CalendarSyncRulesPanel";
|
import CalendarSyncRulesPanel from "./CalendarSyncRulesPanel";
|
||||||
import { ViewStyle, KanbanStage, Task } from "./WeeklyView";
|
import { ViewStyle, KanbanStage, Task } from "./WeeklyView";
|
||||||
import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants";
|
import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants";
|
||||||
@ -13,14 +13,18 @@ import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewCo
|
|||||||
import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route";
|
import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route";
|
||||||
import {
|
import {
|
||||||
ArrowLeftRight,
|
ArrowLeftRight,
|
||||||
|
Briefcase,
|
||||||
Calendar,
|
Calendar,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
|
Check,
|
||||||
|
FolderOpen,
|
||||||
Globe,
|
Globe,
|
||||||
Info,
|
Info,
|
||||||
Kanban,
|
Kanban,
|
||||||
Link,
|
Link,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
Palette,
|
Palette,
|
||||||
|
Pencil,
|
||||||
Play,
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
@ -28,6 +32,25 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import IconPicker from "./IconPicker";
|
||||||
|
import { allIcons } from "./iconRegistry";
|
||||||
|
import Icon from "@mdi/react";
|
||||||
|
|
||||||
|
// Minimal ProjectIcon — resolves an icon name from the unified registry.
|
||||||
|
function ProjectIcon({ icon, size = 18, color }: { icon?: string | null; size?: number; color?: string }) {
|
||||||
|
if (!icon) return <FontAwesomeIcon icon={faFolder} style={{ fontSize: size, color }} />;
|
||||||
|
const normalised = icon.startsWith("fa") && icon.length > 2 && icon[2] === icon[2].toUpperCase()
|
||||||
|
? icon.slice(2, 3).toLowerCase() + icon.slice(3)
|
||||||
|
: icon;
|
||||||
|
const found = allIcons.find((i) => i.name === normalised || i.name === icon);
|
||||||
|
if (found) {
|
||||||
|
if (found.type === "fa") {
|
||||||
|
return <FontAwesomeIcon icon={found.icon as any} style={{ fontSize: size, color }} />;
|
||||||
|
}
|
||||||
|
return <Icon path={found.icon as string} size={size / 24} color={color} />;
|
||||||
|
}
|
||||||
|
return <span style={{ fontSize: size, lineHeight: 1 }}>{icon}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SomedayList {
|
export interface SomedayList {
|
||||||
id: string;
|
id: string;
|
||||||
@ -193,7 +216,7 @@ interface SettingsSidebarProps {
|
|||||||
fetchAvailableTaskLists: (
|
fetchAvailableTaskLists: (
|
||||||
provider: "google" | "apple" | "outlook" | "synology",
|
provider: "google" | "apple" | "outlook" | "synology",
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "sync";
|
initialTab?: "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "sync" | "projects";
|
||||||
projects: { id: string; name: string; icon?: string | null; color?: string | null }[];
|
projects: { id: string; name: string; icon?: string | null; color?: string | null }[];
|
||||||
onProjectsChanged: () => void;
|
onProjectsChanged: () => void;
|
||||||
kanbanStages: KanbanStage[];
|
kanbanStages: KanbanStage[];
|
||||||
@ -326,7 +349,7 @@ function SettingsSidebar({
|
|||||||
onRunSetupAssistant,
|
onRunSetupAssistant,
|
||||||
}: SettingsSidebarProps) {
|
}: SettingsSidebarProps) {
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "sync"
|
"calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" | "sync" | "projects"
|
||||||
>(initialTab || "general");
|
>(initialTab || "general");
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSyncing, setIsSyncing] = useState(false);
|
const [isSyncing, setIsSyncing] = useState(false);
|
||||||
@ -831,6 +854,7 @@ function SettingsSidebar({
|
|||||||
{ key: "localisation", icon: <Globe size={18} />, label: t.localisation },
|
{ key: "localisation", icon: <Globe size={18} />, label: t.localisation },
|
||||||
{ key: "calendar", icon: <Link size={18} />, label: t.calendar },
|
{ key: "calendar", icon: <Link size={18} />, label: t.calendar },
|
||||||
{ key: "sync", icon: <ArrowLeftRight size={18} />, label: t.calendarSync || "Sync" },
|
{ key: "sync", icon: <ArrowLeftRight size={18} />, label: t.calendarSync || "Sync" },
|
||||||
|
{ key: "projects", icon: <Briefcase size={18} />, label: t.projects || "Projects" },
|
||||||
{ key: "account", icon: <User size={18} />, label: t.account },
|
{ key: "account", icon: <User size={18} />, label: t.account },
|
||||||
{ key: "styling", icon: <Palette size={18} />, label: t.styling },
|
{ key: "styling", icon: <Palette size={18} />, label: t.styling },
|
||||||
{ key: "motivation", icon: <Sparkles size={18} />, label: t.motivation },
|
{ key: "motivation", icon: <Sparkles size={18} />, label: t.motivation },
|
||||||
@ -1365,6 +1389,37 @@ function SettingsSidebar({
|
|||||||
<label htmlFor="showProjectIcons" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.showProjectIcons}</label>
|
<label htmlFor="showProjectIcons" style={{ fontSize: "0.9rem", fontWeight: 600 }}>{t.showProjectIcons}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<input type="checkbox" id="showPriorityIcons"
|
||||||
|
checked={profile.showPriorityIcons !== false}
|
||||||
|
onChange={(e) => {
|
||||||
|
saveField("showPriorityIcons", e.target.checked);
|
||||||
|
perView.saveViewSetting("showPriorityIcons", e.target.checked, false);
|
||||||
|
}}
|
||||||
|
style={{ width: "16px", height: "16px" }} />
|
||||||
|
<label htmlFor="showPriorityIcons" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
|
||||||
|
{profile.language === "de" ? "Prioritäts-Icons anzeigen" : "Show Priority Icons"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<label htmlFor="priorityStyle" style={{ fontSize: "0.9rem", fontWeight: 600 }}>
|
||||||
|
{profile.language === "de" ? "Prioritäts-Stil" : "Priority Style"}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="priorityStyle"
|
||||||
|
value={profile.priorityStyle || "eisenhower"}
|
||||||
|
onChange={(e) => saveField("priorityStyle", e.target.value)}
|
||||||
|
className="weekly-input"
|
||||||
|
style={{ padding: "4px 8px", fontSize: "0.85rem", borderRadius: "6px", border: "1px solid var(--weekly-border, #e5e7eb)" }}
|
||||||
|
>
|
||||||
|
<option value="eisenhower">Eisenhower</option>
|
||||||
|
<option value="abcde">ABCDE</option>
|
||||||
|
<option value="ivylee">Ivy Lee</option>
|
||||||
|
<option value="pareto">80/20 (Pareto)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{viewStyle !== "kanban" && (
|
{viewStyle !== "kanban" && (
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
<input type="checkbox" id="protectEventTimes" checked={profile.protectEventTimes || false}
|
<input type="checkbox" id="protectEventTimes" checked={profile.protectEventTimes || false}
|
||||||
@ -3928,6 +3983,165 @@ function SettingsSidebar({
|
|||||||
connections={connections}
|
connections={connections}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
) : activeTab === "projects" ? (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontSize: "1rem", fontWeight: 600, margin: 0, display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<FolderOpen size={18} /> {t.projects || "Projects"}
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: "0.8rem", color: "var(--weekly-settings-label)", marginTop: "4px" }}>
|
||||||
|
{t.projectsDesc || "Organize tasks with color-coded projects"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Existing projects list */}
|
||||||
|
{projects.length === 0 ? (
|
||||||
|
<div style={{ textAlign: "center", padding: "24px 12px", border: "1px dashed var(--weekly-border, #e5e7eb)", borderRadius: "10px" }}>
|
||||||
|
<ProjectIcon icon="folder" size={28} color="#aaa" />
|
||||||
|
<p style={{ fontSize: "0.85rem", color: "#aaa", fontStyle: "italic", marginTop: "8px" }}>
|
||||||
|
{t.noProjects || "No projects yet"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||||
|
{projects.map((p) => (
|
||||||
|
<div key={p.id} style={{
|
||||||
|
display: "flex", alignItems: "center", gap: "10px",
|
||||||
|
padding: "10px 14px", borderRadius: "10px",
|
||||||
|
background: "var(--weekly-bg-soft, #f9fafb)",
|
||||||
|
borderLeft: `4px solid ${p.color || "#999"}`,
|
||||||
|
}}>
|
||||||
|
{editingProjectId === p.id ? (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<button onClick={() => setShowEditProjectIconPicker(!showEditProjectIconPicker)} style={{ width: "38px", height: "38px", borderRadius: "8px", border: "1px solid var(--weekly-border, #e5e7eb)", background: "var(--weekly-bg, #fff)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<ProjectIcon icon={editProjectIcon} size={16} color="#555" />
|
||||||
|
</button>
|
||||||
|
{showEditProjectIconPicker && (
|
||||||
|
<div style={{ position: "absolute", top: "100%", left: 0, marginTop: "4px", zIndex: 50 }}>
|
||||||
|
<IconPicker selectedIcon={editProjectIcon} onSelect={(name) => { setEditProjectIcon(name); setShowEditProjectIconPicker(false); }} darkMode={false} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editProjectName}
|
||||||
|
onChange={(e) => setEditProjectName(e.target.value)}
|
||||||
|
className="weekly-input"
|
||||||
|
style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }) })
|
||||||
|
.then(() => { onProjectsChanged(); setEditingProjectId(null); });
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") setEditingProjectId(null);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<input type="color" value={editProjectColor} onChange={(e) => setEditProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} />
|
||||||
|
<span style={{ fontSize: "0.8rem", color: "var(--weekly-settings-label)" }}>{profile.language === "de" ? "Farbe" : "Color"}</span>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<button onClick={() => setEditingProjectId(null)} style={{ padding: "6px 12px", fontSize: "0.8rem", background: "none", border: "1px solid var(--weekly-border, #ddd)", borderRadius: "8px", cursor: "pointer" }}>
|
||||||
|
{profile.language === "de" ? "Abbrechen" : "Cancel"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
fetch("/api/projects", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: p.id, name: editProjectName, color: editProjectColor, icon: editProjectIcon }) })
|
||||||
|
.then(() => { onProjectsChanged(); setEditingProjectId(null); });
|
||||||
|
}}
|
||||||
|
className="weekly-btn-primary"
|
||||||
|
style={{ padding: "6px 12px", fontSize: "0.8rem", display: "inline-flex", alignItems: "center", gap: "4px" }}
|
||||||
|
>
|
||||||
|
<Check size={14} /> {profile.language === "de" ? "Speichern" : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ProjectIcon icon={p.icon} size={20} color={p.color || "#999"} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<span style={{ fontSize: "0.9rem", fontWeight: 600, display: "block" }}>{p.name}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingProjectId(p.id);
|
||||||
|
setEditProjectName(p.name);
|
||||||
|
setEditProjectColor(p.color || "#3b82f6");
|
||||||
|
setEditProjectIcon(p.icon || "folder");
|
||||||
|
setShowEditProjectIconPicker(false);
|
||||||
|
}}
|
||||||
|
style={{ padding: "6px", opacity: 0.6, cursor: "pointer", background: "none", border: "none", borderRadius: "6px" }}
|
||||||
|
title={profile.language === "de" ? "Bearbeiten" : "Edit"}
|
||||||
|
>
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const msg = profile.language === "de" ? `Projekt "${p.name}" löschen?` : `Delete project "${p.name}"?`;
|
||||||
|
if (confirm(msg)) {
|
||||||
|
fetch(`/api/projects?id=${p.id}`, { method: "DELETE" }).then(() => onProjectsChanged());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ padding: "6px", opacity: 0.6, cursor: "pointer", color: "#ef4444", background: "none", border: "none", borderRadius: "6px" }}
|
||||||
|
title={profile.language === "de" ? "Löschen" : "Delete"}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add new project form */}
|
||||||
|
<div style={{ borderTop: "1px solid var(--weekly-border, #e5e7eb)", paddingTop: "16px" }}>
|
||||||
|
<p style={{ fontSize: "0.8rem", fontWeight: 600, color: "var(--weekly-settings-label)", marginBottom: "10px" }}>
|
||||||
|
{profile.language === "de" ? "Projekt hinzufügen" : "Add Project"}
|
||||||
|
</p>
|
||||||
|
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
||||||
|
<div style={{ position: "relative" }}>
|
||||||
|
<button onClick={() => setShowNewProjectIconPicker(!showNewProjectIconPicker)} style={{ width: "38px", height: "38px", borderRadius: "8px", border: "1px solid var(--weekly-border, #e5e7eb)", background: "var(--weekly-bg, #fff)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<ProjectIcon icon={newProjectIcon} size={16} color="#555" />
|
||||||
|
</button>
|
||||||
|
{showNewProjectIconPicker && (
|
||||||
|
<div style={{ position: "absolute", bottom: "100%", left: 0, marginBottom: "4px", zIndex: 50 }}>
|
||||||
|
<IconPicker selectedIcon={newProjectIcon} onSelect={(name) => { setNewProjectIcon(name); setShowNewProjectIconPicker(false); }} darkMode={false} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input type="color" value={newProjectColor} onChange={(e) => setNewProjectColor(e.target.value)} style={{ width: "30px", height: "30px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newProjectName}
|
||||||
|
onChange={(e) => setNewProjectName(e.target.value)}
|
||||||
|
placeholder={profile.language === "de" ? "Name" : "Name"}
|
||||||
|
className="weekly-input"
|
||||||
|
style={{ flex: 1, padding: "8px 12px", fontSize: "0.9rem" }}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && newProjectName.trim()) {
|
||||||
|
fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) })
|
||||||
|
.then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!newProjectName.trim()) return;
|
||||||
|
fetch("/api/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProjectName.trim(), color: newProjectColor, icon: newProjectIcon }) })
|
||||||
|
.then(() => { onProjectsChanged(); setNewProjectName(""); setNewProjectIcon("folder"); });
|
||||||
|
}}
|
||||||
|
className="weekly-btn-primary"
|
||||||
|
style={{ padding: "8px 14px", fontSize: "0.85rem", display: "inline-flex", alignItems: "center", gap: "4px", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
<Plus size={14} /> {profile.language === "de" ? "Hinzufügen" : "Add"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : activeTab === "about" ? (
|
) : activeTab === "about" ? (
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", flexDirection: "column", gap: "20px" }}
|
style={{ display: "flex", flexDirection: "column", gap: "20px" }}
|
||||||
|
|||||||
@ -89,6 +89,7 @@ import {
|
|||||||
Archive,
|
Archive,
|
||||||
Bot,
|
Bot,
|
||||||
Printer,
|
Printer,
|
||||||
|
Star,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
const stripHtml = (html: string) => html.replace(/<[^>]*>/g, '').trim();
|
const stripHtml = (html: string) => html.replace(/<[^>]*>/g, '').trim();
|
||||||
@ -116,7 +117,7 @@ const PriorityView = dynamic(() => import("./PriorityView"), { ssr: false });
|
|||||||
const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"];
|
const DEVICE_SETTINGS_KEYS = ["viewDays", "cellDuration", "startHour", "endHour", "fontSize", "showSubHourSlots"];
|
||||||
|
|
||||||
// Per-view toggle keys that are also device-specific (sidebar eye toggles)
|
// Per-view toggle keys that are also device-specific (sidebar eye toggles)
|
||||||
const DEVICE_VIEW_SETTINGS_KEYS = ["showSomeday", "showAllDayEvents", "showTaskCheckboxes", "showProjectIcons", "weatherEnabled"] as const;
|
const DEVICE_VIEW_SETTINGS_KEYS = ["showSomeday", "showAllDayEvents", "showTaskCheckboxes", "showProjectIcons", "showPriorityIcons", "weatherEnabled"] as const;
|
||||||
type DeviceViewSettingKey = typeof DEVICE_VIEW_SETTINGS_KEYS[number];
|
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)
|
||||||
@ -222,6 +223,8 @@ export interface SomedayList {
|
|||||||
title: string;
|
title: string;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
tab?: string | null;
|
tab?: string | null;
|
||||||
|
color?: string | null;
|
||||||
|
icon?: string | null;
|
||||||
externalProvider?: string | null;
|
externalProvider?: string | null;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
externalListId?: string | null;
|
externalListId?: string | null;
|
||||||
@ -712,6 +715,9 @@ export default function WeeklyView() {
|
|||||||
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
|
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
|
||||||
const [draggingListId, setDraggingListId] = useState<string | null>(null);
|
const [draggingListId, setDraggingListId] = useState<string | null>(null);
|
||||||
const [listToDelete, setListToDelete] = useState<string | null>(null);
|
const [listToDelete, setListToDelete] = useState<string | null>(null);
|
||||||
|
// Punkt 7+8: per-list color/icon edit popover, and per-tab settings popover
|
||||||
|
const [editingListVisualsId, setEditingListVisualsId] = useState<string | null>(null);
|
||||||
|
const [editingTabVisualsName, setEditingTabVisualsName] = useState<string | null>(null);
|
||||||
const [activeSomedayTab, setActiveSomedayTab] = useState<string | null>(null);
|
const [activeSomedayTab, setActiveSomedayTab] = useState<string | null>(null);
|
||||||
const [editingTabName, setEditingTabName] = useState<string | null>(null);
|
const [editingTabName, setEditingTabName] = useState<string | null>(null);
|
||||||
const [renamingTabValue, setRenamingTabValue] = useState("");
|
const [renamingTabValue, setRenamingTabValue] = useState("");
|
||||||
@ -827,6 +833,44 @@ export default function WeeklyView() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update a list's color or icon, and persist to the API.
|
||||||
|
const updateListVisuals = async (listId: string, updates: { color?: string | null; icon?: string | null }) => {
|
||||||
|
setSomedayLists(prev => prev.map(l => l.id === listId ? { ...l, ...updates } : l));
|
||||||
|
try {
|
||||||
|
await fetch("/api/someday-lists", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id: listId, ...updates }),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to update list visuals:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tab visuals are not first-class entities — store color/icon in user.viewSettings JSON.
|
||||||
|
const getTabVisuals = (tabName: string): { color?: string; icon?: string } => {
|
||||||
|
const cfg = (viewSettingsRef.current as any).somedayTabConfig || {};
|
||||||
|
return cfg[tabName] || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTabVisuals = (tabName: string, updates: { color?: string | null; icon?: string | null }) => {
|
||||||
|
const cfg = { ...((viewSettingsRef.current as any).somedayTabConfig || {}) };
|
||||||
|
const existing = cfg[tabName] || {};
|
||||||
|
const next: any = { ...existing, ...updates };
|
||||||
|
if (!next.color) delete next.color;
|
||||||
|
if (!next.icon) delete next.icon;
|
||||||
|
if (Object.keys(next).length === 0) delete cfg[tabName];
|
||||||
|
else cfg[tabName] = next;
|
||||||
|
const updated = { ...(viewSettingsRef.current as any), somedayTabConfig: cfg };
|
||||||
|
viewSettingsRef.current = updated;
|
||||||
|
setViewSettings(updated);
|
||||||
|
fetch("/api/user/profile", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ viewSettings: updated }),
|
||||||
|
}).catch(e => console.error("[tabs] Failed to save tab visuals:", e));
|
||||||
|
};
|
||||||
|
|
||||||
const dissolveTab = async (tabName: string) => {
|
const dissolveTab = async (tabName: string) => {
|
||||||
const listsToUpdate = somedayLists.filter(l => l.tab === tabName);
|
const listsToUpdate = somedayLists.filter(l => l.tab === tabName);
|
||||||
setSomedayLists(prev => prev.map(l => l.tab === tabName ? { ...l, tab: null } : l));
|
setSomedayLists(prev => prev.map(l => l.tab === tabName ? { ...l, tab: null } : l));
|
||||||
@ -1040,6 +1084,7 @@ export default function WeeklyView() {
|
|||||||
weatherDisplay?: WeatherDisplayKey[];
|
weatherDisplay?: WeatherDisplayKey[];
|
||||||
showTaskCheckboxes?: boolean;
|
showTaskCheckboxes?: boolean;
|
||||||
showProjectIcons?: boolean;
|
showProjectIcons?: boolean;
|
||||||
|
showPriorityIcons?: boolean;
|
||||||
showSomeday?: boolean;
|
showSomeday?: boolean;
|
||||||
showAllDayEvents?: boolean;
|
showAllDayEvents?: boolean;
|
||||||
allDayPosition?: "above" | "below";
|
allDayPosition?: "above" | "below";
|
||||||
@ -1048,7 +1093,7 @@ export default function WeeklyView() {
|
|||||||
startHour?: number;
|
startHour?: number;
|
||||||
endHour?: number;
|
endHour?: number;
|
||||||
};
|
};
|
||||||
const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "weatherDisplay", "showTaskCheckboxes", "showProjectIcons", "showSomeday", "showAllDayEvents", "allDayPosition", "showCompletedTasks", "cellDuration", "startHour", "endHour"] as const;
|
const PER_VIEW_KEYS = ["hourLabelFormat", "showSubHourSlots", "weatherEnabled", "weatherDisplay", "showTaskCheckboxes", "showProjectIcons", "showPriorityIcons", "showSomeday", "showAllDayEvents", "allDayPosition", "showCompletedTasks", "cellDuration", "startHour", "endHour"] as const;
|
||||||
const [viewSettings, setViewSettings] = useState<Record<string, PerViewOverrides>>({});
|
const [viewSettings, setViewSettings] = useState<Record<string, PerViewOverrides>>({});
|
||||||
const viewSettingsRef = useRef<Record<string, PerViewOverrides>>({});
|
const viewSettingsRef = useRef<Record<string, PerViewOverrides>>({});
|
||||||
viewSettingsRef.current = viewSettings;
|
viewSettingsRef.current = viewSettings;
|
||||||
@ -1139,6 +1184,8 @@ export default function WeeklyView() {
|
|||||||
const effectiveWeatherDisplay = (getEffective("weatherDisplay", WEATHER_DISPLAY_DEFAULTS) || WEATHER_DISPLAY_DEFAULTS) as WeatherDisplayKey[];
|
const effectiveWeatherDisplay = (getEffective("weatherDisplay", WEATHER_DISPLAY_DEFAULTS) || WEATHER_DISPLAY_DEFAULTS) as WeatherDisplayKey[];
|
||||||
const effectiveShowTaskCheckboxes = getEffective("showTaskCheckboxes", profile.showTaskCheckboxes);
|
const effectiveShowTaskCheckboxes = getEffective("showTaskCheckboxes", profile.showTaskCheckboxes);
|
||||||
const effectiveShowProjectIcons = getEffective("showProjectIcons", profile.showProjectIcons);
|
const effectiveShowProjectIcons = getEffective("showProjectIcons", profile.showProjectIcons);
|
||||||
|
const effectiveShowPriorityIcons = getEffective("showPriorityIcons", profile.showPriorityIcons !== false) as boolean;
|
||||||
|
const effectivePriorityStyle = (profile.priorityStyle || "eisenhower") as string;
|
||||||
const effectiveShowSomeday = getEffective("showSomeday", profile.showSomeday ?? true);
|
const effectiveShowSomeday = getEffective("showSomeday", profile.showSomeday ?? true);
|
||||||
const effectiveShowAllDay = getEffective("showAllDayEvents", profile.showAllDayEvents ?? true);
|
const effectiveShowAllDay = getEffective("showAllDayEvents", profile.showAllDayEvents ?? true);
|
||||||
const effectiveAllDayPosition = getEffective("allDayPosition", profile.allDayPosition ?? "above") || "above";
|
const effectiveAllDayPosition = getEffective("allDayPosition", profile.allDayPosition ?? "above") || "above";
|
||||||
@ -2562,6 +2609,8 @@ export default function WeeklyView() {
|
|||||||
id: l.id,
|
id: l.id,
|
||||||
title: l.title,
|
title: l.title,
|
||||||
tab: l.tab || null,
|
tab: l.tab || null,
|
||||||
|
color: l.color || null,
|
||||||
|
icon: l.icon || null,
|
||||||
tasks: l.tasks || [],
|
tasks: l.tasks || [],
|
||||||
externalId: l.externalId || null,
|
externalId: l.externalId || null,
|
||||||
externalProvider: l.externalProvider || null,
|
externalProvider: l.externalProvider || null,
|
||||||
@ -2625,6 +2674,8 @@ export default function WeeklyView() {
|
|||||||
id: l.id,
|
id: l.id,
|
||||||
title: l.title,
|
title: l.title,
|
||||||
tab: l.tab || null,
|
tab: l.tab || null,
|
||||||
|
color: l.color || null,
|
||||||
|
icon: l.icon || null,
|
||||||
tasks: [],
|
tasks: [],
|
||||||
externalId: l.externalId || null,
|
externalId: l.externalId || null,
|
||||||
externalProvider: l.externalProvider || null,
|
externalProvider: l.externalProvider || null,
|
||||||
@ -6253,6 +6304,11 @@ export default function WeeklyView() {
|
|||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
language={profile.language}
|
language={profile.language}
|
||||||
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
|
onUpdateTask={async (id, fields) => { await updateTaskFields(id, fields as any); }}
|
||||||
|
initialMethod={(profile.priorityStyle || "eisenhower") as any}
|
||||||
|
onMethodChange={(m) => {
|
||||||
|
setProfile((p: any) => ({ ...p, priorityStyle: m }));
|
||||||
|
saveSetting("priorityStyle", m);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -6345,11 +6401,11 @@ export default function WeeklyView() {
|
|||||||
className="kanban-card-checkbox"
|
className="kanban-card-checkbox"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(() => {
|
{effectiveShowPriorityIcons && (() => {
|
||||||
const pm = getPriorityMeta(task);
|
const pb = getPriorityBadge(task, profile.priorityStyle || "eisenhower", 12);
|
||||||
return pm ? (
|
return pb ? (
|
||||||
<span title={pm.label} style={{ flexShrink: 0, display: "flex", alignItems: "center", marginRight: "4px" }}>
|
<span title={pb.label} style={{ flexShrink: 0, display: "flex", alignItems: "center", marginRight: "4px" }}>
|
||||||
<pm.Icon size={12} color={pm.color} />
|
{pb.node}
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
@ -7164,6 +7220,8 @@ export default function WeeklyView() {
|
|||||||
workingHoursStart={workingHoursStart}
|
workingHoursStart={workingHoursStart}
|
||||||
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
||||||
showProjectIcons={effectiveShowProjectIcons}
|
showProjectIcons={effectiveShowProjectIcons}
|
||||||
|
showPriorityIcons={effectiveShowPriorityIcons}
|
||||||
|
priorityStyle={effectivePriorityStyle}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
onProjectAssign={assignProject}
|
onProjectAssign={assignProject}
|
||||||
kanbanStages={kanbanStages}
|
kanbanStages={kanbanStages}
|
||||||
@ -7582,6 +7640,8 @@ export default function WeeklyView() {
|
|||||||
onSetEditingTaskId={setEditingTaskId}
|
onSetEditingTaskId={setEditingTaskId}
|
||||||
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
||||||
showProjectIcons={effectiveShowProjectIcons}
|
showProjectIcons={effectiveShowProjectIcons}
|
||||||
|
showPriorityIcons={effectiveShowPriorityIcons}
|
||||||
|
priorityStyle={effectivePriorityStyle}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
onProjectAssign={assignProject}
|
onProjectAssign={assignProject}
|
||||||
kanbanStages={kanbanStages}
|
kanbanStages={kanbanStages}
|
||||||
@ -7770,6 +7830,7 @@ export default function WeeklyView() {
|
|||||||
<div
|
<div
|
||||||
key={tab}
|
key={tab}
|
||||||
className="someday-tab-wrapper-h"
|
className="someday-tab-wrapper-h"
|
||||||
|
style={{ position: "relative" }}
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
if (e.dataTransfer.types.includes("text/list-id")) {
|
if (e.dataTransfer.types.includes("text/list-id")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -7790,6 +7851,9 @@ export default function WeeklyView() {
|
|||||||
setDragOverTab(null);
|
setDragOverTab(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{(() => {
|
||||||
|
const tv = getTabVisuals(tab);
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""} ${dragOverTab === tab ? "drag-over" : ""}`}
|
className={`someday-tab-btn-h ${activeSomedayTab === tab ? "active" : ""} ${dragOverTab === tab ? "drag-over" : ""}`}
|
||||||
onClick={() => setSomedayTab(tab)}
|
onClick={() => setSomedayTab(tab)}
|
||||||
@ -7797,13 +7861,42 @@ export default function WeeklyView() {
|
|||||||
setEditingTabName(tab);
|
setEditingTabName(tab);
|
||||||
setRenamingTabValue(tab);
|
setRenamingTabValue(tab);
|
||||||
}}
|
}}
|
||||||
|
style={tv.color ? {
|
||||||
|
background: activeSomedayTab === tab ? tv.color : `${tv.color}22`,
|
||||||
|
color: activeSomedayTab === tab ? "#fff" : tv.color,
|
||||||
|
borderColor: tv.color,
|
||||||
|
} : undefined}
|
||||||
title={t.renameTab}
|
title={t.renameTab}
|
||||||
>{tab} <span className="someday-tab-count">{somedayLists.filter(l => l.tab === tab).length}</span></button>
|
>
|
||||||
|
{tv.icon && (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", marginRight: "4px" }}>
|
||||||
|
<ProjectIcon icon={tv.icon} size={12} color={activeSomedayTab === tab && tv.color ? "#fff" : (tv.color || "currentColor")} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{tab} <span className="someday-tab-count">{somedayLists.filter(l => l.tab === tab).length}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setEditingTabVisualsName(editingTabVisualsName === tab ? null : tab); }}
|
||||||
|
title={profile.language === "de" ? "Tab-Stil bearbeiten" : "Edit tab style"}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: "#bbb", display: "inline-flex", alignItems: "center" }}
|
||||||
|
><Pencil size={10} /></button>
|
||||||
<button
|
<button
|
||||||
className="someday-tab-dissolve-h"
|
className="someday-tab-dissolve-h"
|
||||||
onClick={(e) => { e.stopPropagation(); dissolveTab(tab); }}
|
onClick={(e) => { e.stopPropagation(); dissolveTab(tab); }}
|
||||||
title={t.dissolveTab}
|
title={t.dissolveTab}
|
||||||
><X size={10} /></button>
|
><X size={10} /></button>
|
||||||
|
{editingTabVisualsName === tab && (
|
||||||
|
<TabVisualsPopover
|
||||||
|
tabName={tab}
|
||||||
|
visuals={getTabVisuals(tab)}
|
||||||
|
darkMode={darkMode}
|
||||||
|
language={profile.language}
|
||||||
|
onChange={(updates) => updateTabVisuals(tab, updates)}
|
||||||
|
onClose={() => setEditingTabVisualsName(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
@ -7858,6 +7951,8 @@ export default function WeeklyView() {
|
|||||||
cursor: "text",
|
cursor: "text",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
position: "relative",
|
||||||
|
...(list.color ? { borderLeft: `4px solid ${list.color}`, paddingLeft: "8px" } : {}),
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
@ -8012,6 +8107,15 @@ export default function WeeklyView() {
|
|||||||
>
|
>
|
||||||
<GripVertical size={14} />
|
<GripVertical size={14} />
|
||||||
</div>
|
</div>
|
||||||
|
{list.icon && (
|
||||||
|
<span
|
||||||
|
onClick={(e) => { e.stopPropagation(); setEditingListVisualsId(list.id); }}
|
||||||
|
title={profile.language === "de" ? "Listen-Stil bearbeiten" : "Edit list style"}
|
||||||
|
style={{ display: "inline-flex", alignItems: "center", marginRight: "6px", cursor: "pointer", flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
<ProjectIcon icon={list.icon} size={14} color={list.color || "var(--weekly-text, #555)"} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={list.title}
|
defaultValue={list.title}
|
||||||
@ -8143,6 +8247,26 @@ export default function WeeklyView() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditingListVisualsId(editingListVisualsId === list.id ? null : list.id);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
border: "none",
|
||||||
|
background: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
color: "#ccc",
|
||||||
|
marginLeft: "4px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "4px"
|
||||||
|
}}
|
||||||
|
title={profile.language === "de" ? "Listen-Stil bearbeiten" : "Edit list style"}
|
||||||
|
>
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="someday-list-delete-btn"
|
className="someday-list-delete-btn"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -8167,6 +8291,16 @@ export default function WeeklyView() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* List visuals popover (Punkt 7+8) */}
|
||||||
|
{editingListVisualsId === list.id && (
|
||||||
|
<ListVisualsPopover
|
||||||
|
list={list}
|
||||||
|
darkMode={darkMode}
|
||||||
|
language={profile.language}
|
||||||
|
onChange={(updates) => updateListVisuals(list.id, updates)}
|
||||||
|
onClose={() => setEditingListVisualsId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="weekly-task-list"
|
className="weekly-task-list"
|
||||||
@ -8283,6 +8417,8 @@ export default function WeeklyView() {
|
|||||||
onSetEditingTaskId={setEditingTaskId}
|
onSetEditingTaskId={setEditingTaskId}
|
||||||
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
||||||
showProjectIcons={effectiveShowProjectIcons}
|
showProjectIcons={effectiveShowProjectIcons}
|
||||||
|
showPriorityIcons={effectiveShowPriorityIcons}
|
||||||
|
priorityStyle={effectivePriorityStyle}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
onProjectAssign={assignProject}
|
onProjectAssign={assignProject}
|
||||||
kanbanStages={kanbanStages}
|
kanbanStages={kanbanStages}
|
||||||
@ -8356,6 +8492,8 @@ export default function WeeklyView() {
|
|||||||
onSetEditingTaskId={setEditingTaskId}
|
onSetEditingTaskId={setEditingTaskId}
|
||||||
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
showTaskCheckboxes={effectiveShowTaskCheckboxes}
|
||||||
showProjectIcons={effectiveShowProjectIcons}
|
showProjectIcons={effectiveShowProjectIcons}
|
||||||
|
showPriorityIcons={effectiveShowPriorityIcons}
|
||||||
|
priorityStyle={effectivePriorityStyle}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
onProjectAssign={assignProject}
|
onProjectAssign={assignProject}
|
||||||
kanbanStages={kanbanStages}
|
kanbanStages={kanbanStages}
|
||||||
@ -9526,31 +9664,78 @@ interface TaskItemProps {
|
|||||||
isSubTask?: boolean;
|
isSubTask?: boolean;
|
||||||
showTaskCheckboxes?: boolean;
|
showTaskCheckboxes?: boolean;
|
||||||
showProjectIcons?: boolean;
|
showProjectIcons?: boolean;
|
||||||
|
showPriorityIcons?: boolean;
|
||||||
|
priorityStyle?: string;
|
||||||
projects?: { id: string; name: string; icon?: string | null; color?: string | null }[];
|
projects?: { id: string; name: string; icon?: string | null; color?: string | null }[];
|
||||||
onProjectAssign?: (taskId: string, projectId: string | null) => void;
|
onProjectAssign?: (taskId: string, projectId: string | null) => void;
|
||||||
kanbanStages?: KanbanStage[];
|
kanbanStages?: KanbanStage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Priority indicator helper ─────────────────────────────────────
|
// ── Priority indicator helper ─────────────────────────────────────
|
||||||
// Returns icon + color for a task based on Eisenhower quadrant (primary)
|
// Renders a small priority badge for a task. The visual style is selected
|
||||||
// or ABCDE grade (fallback). Returns null when no priority is set.
|
// by the user's `priorityStyle` setting:
|
||||||
function getPriorityMeta(task: Task): { Icon: React.ElementType; color: string; label: string } | null {
|
// - "eisenhower" → quadrant icons + colors (urgency × importance)
|
||||||
if (task.urgency != null && task.importance != null) {
|
// - "abcde" → letter A–E in a colored circle
|
||||||
if (task.urgency && task.importance) return { Icon: Zap, color: "#ef4444", label: "Sofort erledigen" };
|
// - "ivylee" → digit 1–6 in a colored circle (rank persisted in priority)
|
||||||
if (!task.urgency && task.importance) return { Icon: CalendarClock, color: "#3b82f6", label: "Planen" };
|
// - "pareto" → star for the "vital few" (importance flag)
|
||||||
if (task.urgency && !task.importance) return { Icon: CornerUpRight, color: "#f97316", label: "Delegieren" };
|
// Returns null when the task has no priority data for the active style.
|
||||||
return { Icon: Archive, color: "#9ca3af", label: "Eliminieren" };
|
type PriorityBadge = { node: React.ReactNode; label: string };
|
||||||
}
|
|
||||||
if (task.priority) {
|
const PRIORITY_NUMBER_COLORS: Record<string, string> = {
|
||||||
const map: Record<string, { Icon: React.ElementType; color: string }> = {
|
"1": "#ef4444", "2": "#f97316", "3": "#eab308",
|
||||||
A: { Icon: Zap, color: "#ef4444" },
|
"4": "#22c55e", "5": "#3b82f6", "6": "#8b5cf6",
|
||||||
B: { Icon: CalendarClock, color: "#3b82f6" },
|
|
||||||
C: { Icon: Clock, color: "#eab308" },
|
|
||||||
D: { Icon: CornerUpRight, color: "#f97316" },
|
|
||||||
E: { Icon: Archive, color: "#9ca3af" },
|
|
||||||
};
|
};
|
||||||
const m = map[task.priority];
|
const PRIORITY_LETTER_COLORS: Record<string, string> = {
|
||||||
return m ? { ...m, label: task.priority } : null;
|
A: "#ef4444", B: "#3b82f6", C: "#eab308", D: "#f97316", E: "#9ca3af",
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeBadgeCircle(text: string, color: string, size: number, label: string): PriorityBadge {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
node: (
|
||||||
|
<span style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: color,
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: Math.max(8, size - 6),
|
||||||
|
fontWeight: 700,
|
||||||
|
lineHeight: 1,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>{text}</span>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPriorityBadge(task: Task, style: string, size = 12): PriorityBadge | null {
|
||||||
|
if (style === "abcde") {
|
||||||
|
if (!task.priority || !PRIORITY_LETTER_COLORS[task.priority]) return null;
|
||||||
|
return makeBadgeCircle(task.priority, PRIORITY_LETTER_COLORS[task.priority], size + 2, task.priority);
|
||||||
|
}
|
||||||
|
if (style === "ivylee") {
|
||||||
|
if (!task.priority || !PRIORITY_NUMBER_COLORS[task.priority]) return null;
|
||||||
|
return makeBadgeCircle(task.priority, PRIORITY_NUMBER_COLORS[task.priority], size + 2, `Ivy Lee #${task.priority}`);
|
||||||
|
}
|
||||||
|
if (style === "pareto") {
|
||||||
|
if (task.importance !== true && task.priority !== "A" && task.priority !== "B") return null;
|
||||||
|
return {
|
||||||
|
label: "Vital Few (80/20)",
|
||||||
|
node: <Star size={size + 1} fill="#eab308" color="#eab308" style={{ flexShrink: 0 }} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// eisenhower (default)
|
||||||
|
if (task.urgency != null && task.importance != null) {
|
||||||
|
if (task.urgency && task.importance)
|
||||||
|
return { label: "Sofort erledigen", node: <Zap size={size} color="#ef4444" style={{ flexShrink: 0 }} /> };
|
||||||
|
if (!task.urgency && task.importance)
|
||||||
|
return { label: "Planen", node: <CalendarClock size={size} color="#3b82f6" style={{ flexShrink: 0 }} /> };
|
||||||
|
if (task.urgency && !task.importance)
|
||||||
|
return { label: "Delegieren", node: <CornerUpRight size={size} color="#f97316" style={{ flexShrink: 0 }} /> };
|
||||||
|
return { label: "Eliminieren", node: <Archive size={size} color="#9ca3af" style={{ flexShrink: 0 }} /> };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -9579,6 +9764,8 @@ function TaskItem({
|
|||||||
isSubTask = false,
|
isSubTask = false,
|
||||||
showTaskCheckboxes = false,
|
showTaskCheckboxes = false,
|
||||||
showProjectIcons = false,
|
showProjectIcons = false,
|
||||||
|
showPriorityIcons = true,
|
||||||
|
priorityStyle = "eisenhower",
|
||||||
projects = [],
|
projects = [],
|
||||||
onProjectAssign,
|
onProjectAssign,
|
||||||
kanbanStages = [],
|
kanbanStages = [],
|
||||||
@ -9842,11 +10029,11 @@ function TaskItem({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(() => {
|
{showPriorityIcons && (() => {
|
||||||
const pm = getPriorityMeta(task);
|
const pb = getPriorityBadge(task, priorityStyle, 11);
|
||||||
return pm ? (
|
return pb ? (
|
||||||
<span title={pm.label} style={{ marginRight: "3px", verticalAlign: "middle", display: "inline-flex", alignItems: "center" }}>
|
<span title={pb.label} style={{ marginRight: "3px", verticalAlign: "middle", display: "inline-flex", alignItems: "center" }}>
|
||||||
<pm.Icon size={11} color={pm.color} />
|
{pb.node}
|
||||||
</span>
|
</span>
|
||||||
) : null;
|
) : null;
|
||||||
})()}
|
})()}
|
||||||
@ -10444,6 +10631,202 @@ function TaskItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Punkt 7+8 — popover for editing a someday list's icon and color.
|
||||||
|
function ListVisualsPopover({
|
||||||
|
list,
|
||||||
|
darkMode,
|
||||||
|
language,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
list: { id: string; title: string; color?: string | null; icon?: string | null };
|
||||||
|
darkMode: boolean;
|
||||||
|
language: string;
|
||||||
|
onChange: (updates: { color?: string | null; icon?: string | null }) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [iconPickerOpen, setIconPickerOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const onDoc = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", onDoc);
|
||||||
|
return () => document.removeEventListener("mousedown", onDoc);
|
||||||
|
}, [onClose]);
|
||||||
|
const de = language === "de";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "32px",
|
||||||
|
right: "4px",
|
||||||
|
zIndex: 50,
|
||||||
|
background: darkMode ? "#1e1e2e" : "#fff",
|
||||||
|
border: `1px solid ${darkMode ? "#444" : "#e5e7eb"}`,
|
||||||
|
borderRadius: "10px",
|
||||||
|
padding: "12px",
|
||||||
|
boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
|
||||||
|
minWidth: "240px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: darkMode ? "#d1d5db" : "#444" }}>
|
||||||
|
{de ? "Listen-Stil" : "List style"}
|
||||||
|
</span>
|
||||||
|
<button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", color: darkMode ? "#9ca3af" : "#888", padding: "2px" }}>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<span style={{ fontSize: "0.75rem", color: darkMode ? "#9ca3af" : "#666", flex: 1 }}>{de ? "Icon" : "Icon"}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setIconPickerOpen((v) => !v)}
|
||||||
|
style={{ width: "32px", height: "32px", borderRadius: "8px", border: `1px solid ${darkMode ? "#444" : "#e5e7eb"}`, background: darkMode ? "#2a2a3a" : "#fff", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ProjectIcon icon={list.icon} size={16} color={list.color || (darkMode ? "#d1d5db" : "#555")} />
|
||||||
|
</button>
|
||||||
|
{list.icon && (
|
||||||
|
<button
|
||||||
|
onClick={() => onChange({ icon: null })}
|
||||||
|
title={de ? "Icon entfernen" : "Remove icon"}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "#ef4444", padding: "2px" }}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{iconPickerOpen && (
|
||||||
|
<div style={{ position: "absolute", top: "78px", right: "12px", zIndex: 60 }}>
|
||||||
|
<IconPicker
|
||||||
|
selectedIcon={list.icon || ""}
|
||||||
|
onSelect={(name) => { onChange({ icon: name }); setIconPickerOpen(false); }}
|
||||||
|
darkMode={darkMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<span style={{ fontSize: "0.75rem", color: darkMode ? "#9ca3af" : "#666", flex: 1 }}>{de ? "Farbe" : "Color"}</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={list.color || "#6366f1"}
|
||||||
|
onChange={(e) => onChange({ color: e.target.value })}
|
||||||
|
style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }}
|
||||||
|
/>
|
||||||
|
{list.color && (
|
||||||
|
<button
|
||||||
|
onClick={() => onChange({ color: null })}
|
||||||
|
title={de ? "Farbe entfernen" : "Remove color"}
|
||||||
|
style={{ background: "none", border: "none", cursor: "pointer", color: "#ef4444", padding: "2px" }}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Punkt 7 — popover for editing a tab's icon and color (stored in viewSettings JSON).
|
||||||
|
function TabVisualsPopover({
|
||||||
|
tabName,
|
||||||
|
visuals,
|
||||||
|
darkMode,
|
||||||
|
language,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
tabName: string;
|
||||||
|
visuals: { color?: string; icon?: string };
|
||||||
|
darkMode: boolean;
|
||||||
|
language: string;
|
||||||
|
onChange: (updates: { color?: string | null; icon?: string | null }) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [iconPickerOpen, setIconPickerOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const onDoc = (e: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", onDoc);
|
||||||
|
return () => document.removeEventListener("mousedown", onDoc);
|
||||||
|
}, [onClose]);
|
||||||
|
const de = language === "de";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "calc(100% + 4px)",
|
||||||
|
left: 0,
|
||||||
|
zIndex: 100,
|
||||||
|
background: darkMode ? "#1e1e2e" : "#fff",
|
||||||
|
border: `1px solid ${darkMode ? "#444" : "#e5e7eb"}`,
|
||||||
|
borderRadius: "10px",
|
||||||
|
padding: "10px",
|
||||||
|
boxShadow: "0 8px 24px rgba(0,0,0,0.18)",
|
||||||
|
minWidth: "220px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "8px",
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<span style={{ fontSize: "0.78rem", fontWeight: 600, color: darkMode ? "#d1d5db" : "#444" }}>
|
||||||
|
{de ? `Tab-Stil: ${tabName}` : `Tab style: ${tabName}`}
|
||||||
|
</span>
|
||||||
|
<button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", color: darkMode ? "#9ca3af" : "#888", padding: "2px" }}>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<span style={{ fontSize: "0.75rem", color: darkMode ? "#9ca3af" : "#666", flex: 1 }}>{de ? "Icon" : "Icon"}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setIconPickerOpen((v) => !v)}
|
||||||
|
style={{ width: "32px", height: "32px", borderRadius: "8px", border: `1px solid ${darkMode ? "#444" : "#e5e7eb"}`, background: darkMode ? "#2a2a3a" : "#fff", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<ProjectIcon icon={visuals.icon} size={16} color={visuals.color || (darkMode ? "#d1d5db" : "#555")} />
|
||||||
|
</button>
|
||||||
|
{visuals.icon && (
|
||||||
|
<button onClick={() => onChange({ icon: null })} title={de ? "Icon entfernen" : "Remove icon"} style={{ background: "none", border: "none", cursor: "pointer", color: "#ef4444", padding: "2px" }}>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{iconPickerOpen && (
|
||||||
|
<div style={{ position: "absolute", top: "70px", left: "10px", zIndex: 110 }}>
|
||||||
|
<IconPicker
|
||||||
|
selectedIcon={visuals.icon || ""}
|
||||||
|
onSelect={(name) => { onChange({ icon: name }); setIconPickerOpen(false); }}
|
||||||
|
darkMode={darkMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
|
<span style={{ fontSize: "0.75rem", color: darkMode ? "#9ca3af" : "#666", flex: 1 }}>{de ? "Farbe" : "Color"}</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={visuals.color || "#6366f1"}
|
||||||
|
onChange={(e) => onChange({ color: e.target.value })}
|
||||||
|
style={{ width: "32px", height: "32px", border: "none", cursor: "pointer", padding: 0, borderRadius: "6px" }}
|
||||||
|
/>
|
||||||
|
{visuals.color && (
|
||||||
|
<button onClick={() => onChange({ color: null })} title={de ? "Farbe entfernen" : "Remove color"} style={{ background: "none", border: "none", cursor: "pointer", color: "#ef4444", padding: "2px" }}>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Projects Sidebar Component
|
// Projects Sidebar Component
|
||||||
const projectIconsGlobal: { name: string; icon: IconDefinition }[] = [
|
const projectIconsGlobal: { name: string; icon: IconDefinition }[] = [
|
||||||
{ name: "folder", icon: faFolder }, { name: "briefcase", icon: faBriefcase },
|
{ name: "folder", icon: faFolder }, { name: "briefcase", icon: faBriefcase },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user