mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
chore: uncommitted WIP from previous session
Includes todo page edit bar, minimized pages store, times shared types, manacore data layer cleanup, and dashboard widget updates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05e5e957e8
commit
4c9b4583bc
8 changed files with 1007 additions and 53 deletions
151
apps/times/packages/shared/src/constants/clock.ts
Normal file
151
apps/times/packages/shared/src/constants/clock.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// Popular timezones with city names and coordinates for map display
|
||||
export const POPULAR_TIMEZONES = [
|
||||
{
|
||||
timezone: 'America/New_York',
|
||||
city: 'New York',
|
||||
region: 'Americas',
|
||||
lat: 40.7128,
|
||||
lng: -74.006,
|
||||
},
|
||||
{
|
||||
timezone: 'America/Los_Angeles',
|
||||
city: 'Los Angeles',
|
||||
region: 'Americas',
|
||||
lat: 34.0522,
|
||||
lng: -118.2437,
|
||||
},
|
||||
{ timezone: 'America/Chicago', city: 'Chicago', region: 'Americas', lat: 41.8781, lng: -87.6298 },
|
||||
{ timezone: 'America/Toronto', city: 'Toronto', region: 'Americas', lat: 43.6532, lng: -79.3832 },
|
||||
{
|
||||
timezone: 'America/Sao_Paulo',
|
||||
city: 'São Paulo',
|
||||
region: 'Americas',
|
||||
lat: -23.5505,
|
||||
lng: -46.6333,
|
||||
},
|
||||
{
|
||||
timezone: 'America/Mexico_City',
|
||||
city: 'Mexico City',
|
||||
region: 'Americas',
|
||||
lat: 19.4326,
|
||||
lng: -99.1332,
|
||||
},
|
||||
{
|
||||
timezone: 'America/Buenos_Aires',
|
||||
city: 'Buenos Aires',
|
||||
region: 'Americas',
|
||||
lat: -34.6037,
|
||||
lng: -58.3816,
|
||||
},
|
||||
{
|
||||
timezone: 'America/Vancouver',
|
||||
city: 'Vancouver',
|
||||
region: 'Americas',
|
||||
lat: 49.2827,
|
||||
lng: -123.1207,
|
||||
},
|
||||
{ timezone: 'Europe/London', city: 'London', region: 'Europe', lat: 51.5074, lng: -0.1278 },
|
||||
{ timezone: 'Europe/Paris', city: 'Paris', region: 'Europe', lat: 48.8566, lng: 2.3522 },
|
||||
{ timezone: 'Europe/Berlin', city: 'Berlin', region: 'Europe', lat: 52.52, lng: 13.405 },
|
||||
{ timezone: 'Europe/Rome', city: 'Rome', region: 'Europe', lat: 41.9028, lng: 12.4964 },
|
||||
{ timezone: 'Europe/Madrid', city: 'Madrid', region: 'Europe', lat: 40.4168, lng: -3.7038 },
|
||||
{ timezone: 'Europe/Amsterdam', city: 'Amsterdam', region: 'Europe', lat: 52.3676, lng: 4.9041 },
|
||||
{ timezone: 'Europe/Vienna', city: 'Vienna', region: 'Europe', lat: 48.2082, lng: 16.3738 },
|
||||
{ timezone: 'Europe/Zurich', city: 'Zurich', region: 'Europe', lat: 47.3769, lng: 8.5417 },
|
||||
{ timezone: 'Europe/Moscow', city: 'Moscow', region: 'Europe', lat: 55.7558, lng: 37.6173 },
|
||||
{ timezone: 'Europe/Stockholm', city: 'Stockholm', region: 'Europe', lat: 59.3293, lng: 18.0686 },
|
||||
{ timezone: 'Europe/Istanbul', city: 'Istanbul', region: 'Europe', lat: 41.0082, lng: 28.9784 },
|
||||
{ timezone: 'Asia/Tokyo', city: 'Tokyo', region: 'Asia', lat: 35.6762, lng: 139.6503 },
|
||||
{ timezone: 'Asia/Shanghai', city: 'Shanghai', region: 'Asia', lat: 31.2304, lng: 121.4737 },
|
||||
{ timezone: 'Asia/Hong_Kong', city: 'Hong Kong', region: 'Asia', lat: 22.3193, lng: 114.1694 },
|
||||
{ timezone: 'Asia/Singapore', city: 'Singapore', region: 'Asia', lat: 1.3521, lng: 103.8198 },
|
||||
{ timezone: 'Asia/Seoul', city: 'Seoul', region: 'Asia', lat: 37.5665, lng: 126.978 },
|
||||
{ timezone: 'Asia/Mumbai', city: 'Mumbai', region: 'Asia', lat: 19.076, lng: 72.8777 },
|
||||
{ timezone: 'Asia/Dubai', city: 'Dubai', region: 'Asia', lat: 25.2048, lng: 55.2708 },
|
||||
{ timezone: 'Asia/Bangkok', city: 'Bangkok', region: 'Asia', lat: 13.7563, lng: 100.5018 },
|
||||
{ timezone: 'Asia/Jakarta', city: 'Jakarta', region: 'Asia', lat: -6.2088, lng: 106.8456 },
|
||||
{ timezone: 'Australia/Sydney', city: 'Sydney', region: 'Oceania', lat: -33.8688, lng: 151.2093 },
|
||||
{
|
||||
timezone: 'Australia/Melbourne',
|
||||
city: 'Melbourne',
|
||||
region: 'Oceania',
|
||||
lat: -37.8136,
|
||||
lng: 144.9631,
|
||||
},
|
||||
{
|
||||
timezone: 'Pacific/Auckland',
|
||||
city: 'Auckland',
|
||||
region: 'Oceania',
|
||||
lat: -36.8485,
|
||||
lng: 174.7633,
|
||||
},
|
||||
{ timezone: 'Africa/Cairo', city: 'Cairo', region: 'Africa', lat: 30.0444, lng: 31.2357 },
|
||||
{
|
||||
timezone: 'Africa/Johannesburg',
|
||||
city: 'Johannesburg',
|
||||
region: 'Africa',
|
||||
lat: -26.2041,
|
||||
lng: 28.0473,
|
||||
},
|
||||
{ timezone: 'Africa/Lagos', city: 'Lagos', region: 'Africa', lat: 6.5244, lng: 3.3792 },
|
||||
] as const;
|
||||
|
||||
// Available alarm sounds
|
||||
export const ALARM_SOUNDS = [
|
||||
{ id: 'default', name: 'Default', nameDE: 'Standard' },
|
||||
{ id: 'gentle', name: 'Gentle', nameDE: 'Sanft' },
|
||||
{ id: 'classic', name: 'Classic', nameDE: 'Klassisch' },
|
||||
{ id: 'digital', name: 'Digital', nameDE: 'Digital' },
|
||||
{ id: 'nature', name: 'Nature', nameDE: 'Natur' },
|
||||
{ id: 'chime', name: 'Chime', nameDE: 'Glockenspiel' },
|
||||
] as const;
|
||||
|
||||
// Countdown timer presets
|
||||
export const QUICK_TIMER_PRESETS = [
|
||||
{ label: '1 min', seconds: 60 },
|
||||
{ label: '3 min', seconds: 180 },
|
||||
{ label: '5 min', seconds: 300 },
|
||||
{ label: '10 min', seconds: 600 },
|
||||
{ label: '15 min', seconds: 900 },
|
||||
{ label: '30 min', seconds: 1800 },
|
||||
{ label: '45 min', seconds: 2700 },
|
||||
{ label: '1 hour', seconds: 3600 },
|
||||
] as const;
|
||||
|
||||
// Default alarm presets
|
||||
export const DEFAULT_ALARM_PRESETS = [
|
||||
{ time: '06:00', label: 'Früh aufstehen', labelEN: 'Wake up early' },
|
||||
{ time: '07:00', label: 'Aufwachen', labelEN: 'Wake up' },
|
||||
{ time: '08:00', label: 'Morgen', labelEN: 'Morning' },
|
||||
{ time: '12:00', label: 'Mittag', labelEN: 'Noon' },
|
||||
{ time: '18:00', label: 'Feierabend', labelEN: 'End of work' },
|
||||
{ time: '22:00', label: 'Schlafenszeit', labelEN: 'Bedtime' },
|
||||
] as const;
|
||||
|
||||
// Pomodoro presets
|
||||
export const POMODORO_PRESETS = [
|
||||
{
|
||||
name: 'Classic Pomodoro',
|
||||
nameDE: 'Klassischer Pomodoro',
|
||||
workDuration: 25 * 60,
|
||||
breakDuration: 5 * 60,
|
||||
longBreakDuration: 15 * 60,
|
||||
sessionsBeforeLongBreak: 4,
|
||||
},
|
||||
{
|
||||
name: 'Short Focus',
|
||||
nameDE: 'Kurzer Fokus',
|
||||
workDuration: 15 * 60,
|
||||
breakDuration: 3 * 60,
|
||||
longBreakDuration: 10 * 60,
|
||||
sessionsBeforeLongBreak: 4,
|
||||
},
|
||||
{
|
||||
name: 'Deep Work',
|
||||
nameDE: 'Tiefes Arbeiten',
|
||||
workDuration: 50 * 60,
|
||||
breakDuration: 10 * 60,
|
||||
longBreakDuration: 30 * 60,
|
||||
sessionsBeforeLongBreak: 3,
|
||||
},
|
||||
] as const;
|
||||
|
|
@ -38,3 +38,7 @@ export const DEFAULT_SETTINGS = {
|
|||
timerReminderMinutes: 0,
|
||||
autoStopTimerHours: 0,
|
||||
};
|
||||
|
||||
// ─── Clock Constants ────────────────────────────────────
|
||||
|
||||
export * from './clock';
|
||||
|
|
|
|||
172
apps/times/packages/shared/src/types/clock.ts
Normal file
172
apps/times/packages/shared/src/types/clock.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// ─── Alarm ──────────────────────────────────────────────
|
||||
|
||||
export interface Alarm {
|
||||
id: string;
|
||||
userId: string;
|
||||
label: string | null;
|
||||
time: string; // HH:mm format
|
||||
enabled: boolean;
|
||||
repeatDays: number[] | null; // [0-6] where 0 = Sunday
|
||||
snoozeMinutes: number | null;
|
||||
sound: string | null;
|
||||
vibrate: boolean | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateAlarmInput {
|
||||
label?: string;
|
||||
time: string;
|
||||
enabled?: boolean;
|
||||
repeatDays?: number[];
|
||||
snoozeMinutes?: number;
|
||||
sound?: string;
|
||||
vibrate?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateAlarmInput {
|
||||
label?: string;
|
||||
time?: string;
|
||||
enabled?: boolean;
|
||||
repeatDays?: number[];
|
||||
snoozeMinutes?: number;
|
||||
sound?: string;
|
||||
vibrate?: boolean;
|
||||
}
|
||||
|
||||
export type RepeatDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
export const REPEAT_DAY_LABELS = {
|
||||
0: 'Sun',
|
||||
1: 'Mon',
|
||||
2: 'Tue',
|
||||
3: 'Wed',
|
||||
4: 'Thu',
|
||||
5: 'Fri',
|
||||
6: 'Sat',
|
||||
} as const;
|
||||
|
||||
export const REPEAT_DAY_LABELS_DE = {
|
||||
0: 'So',
|
||||
1: 'Mo',
|
||||
2: 'Di',
|
||||
3: 'Mi',
|
||||
4: 'Do',
|
||||
5: 'Fr',
|
||||
6: 'Sa',
|
||||
} as const;
|
||||
|
||||
// ─── Countdown Timer ────────────────────────────────────
|
||||
|
||||
export type CountdownTimerStatus = 'idle' | 'running' | 'paused' | 'finished';
|
||||
|
||||
export interface CountdownTimer {
|
||||
id: string;
|
||||
userId: string;
|
||||
label: string | null;
|
||||
durationSeconds: number;
|
||||
remainingSeconds: number | null;
|
||||
status: CountdownTimerStatus;
|
||||
startedAt: string | null;
|
||||
pausedAt: string | null;
|
||||
sound: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateCountdownTimerInput {
|
||||
label?: string;
|
||||
durationSeconds: number;
|
||||
sound?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCountdownTimerInput {
|
||||
label?: string;
|
||||
durationSeconds?: number;
|
||||
sound?: string;
|
||||
}
|
||||
|
||||
export function formatCountdownDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function parseCountdownDuration(formatted: string): number {
|
||||
const parts = formatted.split(':').map(Number);
|
||||
if (parts.length === 3) {
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
}
|
||||
if (parts.length === 2) {
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
// ─── World Clock ────────────────────────────────────────
|
||||
|
||||
export interface WorldClock {
|
||||
id: string;
|
||||
userId: string;
|
||||
timezone: string; // IANA timezone e.g. 'America/New_York'
|
||||
cityName: string;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreateWorldClockInput {
|
||||
timezone: string;
|
||||
cityName: string;
|
||||
}
|
||||
|
||||
export interface TimezoneInfo {
|
||||
timezone: string;
|
||||
city: string;
|
||||
}
|
||||
|
||||
// ─── Preset ─────────────────────────────────────────────
|
||||
|
||||
export type PresetType = 'timer' | 'pomodoro';
|
||||
|
||||
export interface PresetSettings {
|
||||
workDuration?: number;
|
||||
breakDuration?: number;
|
||||
longBreakDuration?: number;
|
||||
sessionsBeforeLongBreak?: number;
|
||||
sound?: string;
|
||||
}
|
||||
|
||||
export interface Preset {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: PresetType;
|
||||
name: string;
|
||||
durationSeconds: number;
|
||||
settings: PresetSettings | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CreatePresetInput {
|
||||
type: PresetType;
|
||||
name: string;
|
||||
durationSeconds: number;
|
||||
settings?: PresetSettings;
|
||||
}
|
||||
|
||||
export interface UpdatePresetInput {
|
||||
name?: string;
|
||||
durationSeconds?: number;
|
||||
settings?: PresetSettings;
|
||||
}
|
||||
|
||||
export const DEFAULT_POMODORO_SETTINGS: PresetSettings = {
|
||||
workDuration: 25 * 60,
|
||||
breakDuration: 5 * 60,
|
||||
longBreakDuration: 15 * 60,
|
||||
sessionsBeforeLongBreak: 4,
|
||||
};
|
||||
|
|
@ -152,3 +152,7 @@ export interface SavedFilter {
|
|||
criteria: FilterCriteria;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// ─── Clock Types ────────────────────────────────────────
|
||||
|
||||
export * from './clock';
|
||||
|
|
|
|||
396
apps/todo/apps/web/src/lib/components/pages/PageEditBar.svelte
Normal file
396
apps/todo/apps/web/src/lib/components/pages/PageEditBar.svelte
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
Warning,
|
||||
Calendar,
|
||||
CalendarDots,
|
||||
CheckCircle,
|
||||
Star,
|
||||
Lightning,
|
||||
Clock,
|
||||
Fire,
|
||||
Leaf,
|
||||
Heart,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Trash,
|
||||
} from '@manacore/shared-icons';
|
||||
import type { PageIcon, PageConfig } from '$lib/stores/settings.svelte';
|
||||
|
||||
interface Props {
|
||||
config: PageConfig;
|
||||
onUpdate: (data: Partial<PageConfig>) => void;
|
||||
onMoveLeft?: () => void;
|
||||
onMoveRight?: () => void;
|
||||
onDelete: () => void;
|
||||
isFirst: boolean;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
let { config, onUpdate, onMoveLeft, onMoveRight, onDelete, isFirst, isLast }: Props = $props();
|
||||
|
||||
const ICONS: { id: PageIcon; component: typeof Warning }[] = [
|
||||
{ id: 'warning', component: Warning },
|
||||
{ id: 'calendar', component: Calendar },
|
||||
{ id: 'calendar-dots', component: CalendarDots },
|
||||
{ id: 'check', component: CheckCircle },
|
||||
{ id: 'star', component: Star },
|
||||
{ id: 'lightning', component: Lightning },
|
||||
{ id: 'clock', component: Clock },
|
||||
{ id: 'fire', component: Fire },
|
||||
{ id: 'leaf', component: Leaf },
|
||||
{ id: 'heart', component: Heart },
|
||||
];
|
||||
|
||||
const PRIORITIES = [
|
||||
{ id: 'urgent' as const, label: 'Dringend', color: '#EF4444' },
|
||||
{ id: 'high' as const, label: 'Hoch', color: '#F59E0B' },
|
||||
{ id: 'medium' as const, label: 'Mittel', color: '#3B82F6' },
|
||||
{ id: 'low' as const, label: 'Niedrig', color: '#6B7280' },
|
||||
];
|
||||
|
||||
const DATE_RANGES = [
|
||||
{ id: 'overdue' as const, label: 'Überfällig' },
|
||||
{ id: 'today' as const, label: 'Heute' },
|
||||
{ id: 'tomorrow' as const, label: 'Morgen' },
|
||||
{ id: 'upcoming' as const, label: 'Demnächst' },
|
||||
{ id: 'any' as const, label: 'Alle' },
|
||||
];
|
||||
|
||||
function togglePriority(p: 'low' | 'medium' | 'high' | 'urgent') {
|
||||
const current = config.filter.priorities ?? [];
|
||||
const next = current.includes(p) ? current.filter((x) => x !== p) : [...current, p];
|
||||
onUpdate({ filter: { ...config.filter, priorities: next.length ? next : undefined } });
|
||||
}
|
||||
|
||||
function setDateRange(range: typeof config.filter.dateRange) {
|
||||
onUpdate({
|
||||
filter: {
|
||||
...config.filter,
|
||||
dateRange: range === config.filter.dateRange ? undefined : range,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCompleted() {
|
||||
onUpdate({ filter: { ...config.filter, completed: !config.filter.completed } });
|
||||
}
|
||||
|
||||
function setIcon(icon: PageIcon) {
|
||||
onUpdate({ icon });
|
||||
}
|
||||
|
||||
let showFilters = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="edit-bar">
|
||||
<!-- Icon picker row -->
|
||||
<div class="edit-row icons-row">
|
||||
{#each ICONS as icon (icon.id)}
|
||||
<button
|
||||
class="icon-btn"
|
||||
class:active={config.icon === icon.id}
|
||||
onclick={() => setIcon(icon.id)}
|
||||
title={icon.id}
|
||||
>
|
||||
<icon.component size={16} weight={config.icon === icon.id ? 'fill' : 'regular'} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Filter toggle -->
|
||||
<button class="filter-toggle" onclick={() => (showFilters = !showFilters)}>
|
||||
<span class="filter-toggle-label">Filter</span>
|
||||
<span class="filter-toggle-arrow" class:open={showFilters}>▾</span>
|
||||
</button>
|
||||
|
||||
<!-- Filter controls -->
|
||||
{#if showFilters}
|
||||
<div class="edit-row filter-section">
|
||||
<!-- Priority filter -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Priorität</span>
|
||||
<div class="filter-pills">
|
||||
{#each PRIORITIES as p (p.id)}
|
||||
<button
|
||||
class="filter-pill"
|
||||
class:active={config.filter.priorities?.includes(p.id)}
|
||||
style="--pill-color: {p.color}"
|
||||
onclick={() => togglePriority(p.id)}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Date range filter -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Zeitraum</span>
|
||||
<div class="filter-pills">
|
||||
{#each DATE_RANGES as dr (dr.id)}
|
||||
<button
|
||||
class="filter-pill"
|
||||
class:active={config.filter.dateRange === dr.id}
|
||||
onclick={() => setDateRange(dr.id)}
|
||||
>
|
||||
{dr.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed toggle -->
|
||||
<div class="filter-group">
|
||||
<label class="completed-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.filter.completed ?? false}
|
||||
onchange={toggleCompleted}
|
||||
/>
|
||||
<span>Nur erledigte</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Actions row -->
|
||||
<div class="edit-row actions-row">
|
||||
<div class="move-btns">
|
||||
{#if !isFirst && onMoveLeft}
|
||||
<button class="action-btn" onclick={onMoveLeft} title="Nach links">
|
||||
<ArrowLeft size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isLast && onMoveRight}
|
||||
<button class="action-btn" onclick={onMoveRight} title="Nach rechts">
|
||||
<ArrowRight size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="action-btn delete-btn" onclick={onDelete} title="Seite löschen">
|
||||
<Trash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.edit-bar {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
animation: slideDown 0.2s ease-out;
|
||||
}
|
||||
:global(.dark) .edit-bar {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Icon picker */
|
||||
.icons-row {
|
||||
gap: 0.125rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
.icon-btn.active {
|
||||
background: var(--color-primary, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
:global(.dark) .icon-btn {
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .icon-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
:global(.dark) .icon-btn.active {
|
||||
background: var(--color-primary, #8b5cf6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Filter toggle */
|
||||
.filter-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.filter-toggle:hover {
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .filter-toggle:hover {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.filter-toggle-arrow {
|
||||
font-size: 0.625rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.filter-toggle-arrow.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Filter section */
|
||||
.filter-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.filter-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.filter-pill:hover {
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
color: #374151;
|
||||
}
|
||||
.filter-pill.active {
|
||||
background: var(--pill-color, var(--color-primary, #8b5cf6));
|
||||
border-color: var(--pill-color, var(--color-primary, #8b5cf6));
|
||||
color: white;
|
||||
}
|
||||
:global(.dark) .filter-pill {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .filter-pill:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
:global(.dark) .filter-pill.active {
|
||||
background: var(--pill-color, var(--color-primary, #8b5cf6));
|
||||
border-color: var(--pill-color, var(--color-primary, #8b5cf6));
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Completed toggle */
|
||||
.completed-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
}
|
||||
.completed-toggle input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: var(--color-primary, #8b5cf6);
|
||||
cursor: pointer;
|
||||
}
|
||||
:global(.dark) .completed-toggle {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Actions row */
|
||||
.actions-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.move-btns {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
:global(.dark) .delete-btn:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
Calendar,
|
||||
TagSimple,
|
||||
X,
|
||||
Plus,
|
||||
} from '@manacore/shared-icons';
|
||||
|
||||
export interface PageOption {
|
||||
|
|
@ -22,10 +23,11 @@
|
|||
interface Props {
|
||||
onSelect: (pageId: string) => void;
|
||||
onClose: () => void;
|
||||
onCreateCustom?: () => void;
|
||||
activePageIds?: string[];
|
||||
}
|
||||
|
||||
let { onSelect, onClose, activePageIds = [] }: Props = $props();
|
||||
let { onSelect, onClose, onCreateCustom, activePageIds = [] }: Props = $props();
|
||||
|
||||
const PAGE_OPTIONS: PageOption[] = [
|
||||
{
|
||||
|
|
@ -113,7 +115,23 @@
|
|||
</button>
|
||||
{/each}
|
||||
|
||||
{#if availableOptions.length === 0}
|
||||
{#if availableOptions.length > 0 && onCreateCustom}
|
||||
<div class="divider"></div>
|
||||
{/if}
|
||||
|
||||
{#if onCreateCustom}
|
||||
<button class="page-option custom-option" onclick={onCreateCustom}>
|
||||
<div class="option-icon custom-icon">
|
||||
<Plus size={20} />
|
||||
</div>
|
||||
<div class="option-text">
|
||||
<span class="option-title">Eigene Seite</span>
|
||||
<span class="option-desc">Seite mit eigenen Filtern erstellen</span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if availableOptions.length === 0 && !onCreateCustom}
|
||||
<div class="empty-state">
|
||||
<p>Alle Seiten sind bereits geöffnet</p>
|
||||
</div>
|
||||
|
|
@ -237,6 +255,15 @@
|
|||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
.custom-icon {
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 10%, transparent);
|
||||
}
|
||||
|
||||
.custom-option {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,72 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { isToday, isPast, startOfDay, addDays, subHours, format } from 'date-fns';
|
||||
import { isToday, isTomorrow, isPast, startOfDay, addDays, subHours, format } from 'date-fns';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { Task } from '@todo/shared';
|
||||
import { X, Circle, Minus, DotsSixVertical, CornersOut, CornersIn } from '@manacore/shared-icons';
|
||||
import {
|
||||
X,
|
||||
Circle,
|
||||
Minus,
|
||||
DotsSixVertical,
|
||||
CornersOut,
|
||||
CornersIn,
|
||||
Warning,
|
||||
Calendar,
|
||||
CalendarDots,
|
||||
CheckCircle,
|
||||
Star,
|
||||
Lightning,
|
||||
Clock,
|
||||
Fire,
|
||||
Leaf,
|
||||
Heart,
|
||||
} from '@manacore/shared-icons';
|
||||
import KanbanTaskCard from '../kanban/KanbanTaskCard.svelte';
|
||||
import PageEditBar from './PageEditBar.svelte';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { todoSettings } from '$lib/stores/settings.svelte';
|
||||
import type { PageConfig, PageIcon } from '$lib/stores/settings.svelte';
|
||||
|
||||
interface Props {
|
||||
pageId: string;
|
||||
title?: string;
|
||||
maximized?: boolean;
|
||||
editMode?: boolean;
|
||||
filterConfig?: PageConfig['filter'];
|
||||
pageIcon?: PageIcon;
|
||||
pageColor?: string;
|
||||
customPageConfig?: PageConfig;
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
onClose: () => void;
|
||||
onMinimize?: () => void;
|
||||
onMaximize?: () => void;
|
||||
onRename?: (name: string) => void;
|
||||
onUpdateConfig?: (data: Partial<PageConfig>) => void;
|
||||
onMoveLeft?: () => void;
|
||||
onMoveRight?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
pageId,
|
||||
title: customTitle,
|
||||
maximized = false,
|
||||
editMode = false,
|
||||
filterConfig,
|
||||
pageIcon,
|
||||
pageColor,
|
||||
customPageConfig,
|
||||
isFirst = false,
|
||||
isLast = false,
|
||||
onClose,
|
||||
onMinimize,
|
||||
onMaximize,
|
||||
onRename,
|
||||
onUpdateConfig,
|
||||
onMoveLeft,
|
||||
onMoveRight,
|
||||
onDelete,
|
||||
}: Props = $props();
|
||||
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
|
|
@ -33,7 +74,6 @@
|
|||
let titleEl = $state<HTMLSpanElement | null>(null);
|
||||
let isTitleFocused = $state(false);
|
||||
|
||||
// Set initial text content without reactive binding (avoids cursor jump)
|
||||
$effect(() => {
|
||||
if (titleEl && !isTitleFocused) {
|
||||
titleEl.textContent = displayTitle;
|
||||
|
|
@ -52,25 +92,84 @@
|
|||
}
|
||||
}
|
||||
|
||||
const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
// Icon mapping
|
||||
const ICON_MAP: Record<PageIcon, typeof Warning> = {
|
||||
warning: Warning,
|
||||
calendar: Calendar,
|
||||
'calendar-dots': CalendarDots,
|
||||
check: CheckCircle,
|
||||
star: Star,
|
||||
lightning: Lightning,
|
||||
clock: Clock,
|
||||
fire: Fire,
|
||||
leaf: Leaf,
|
||||
heart: Heart,
|
||||
};
|
||||
|
||||
const PAGE_META: Record<string, { title: string; color: string; icon?: PageIcon }> = {
|
||||
todo: { title: 'To Do', color: '#6B7280' },
|
||||
completed: { title: 'Erledigt', color: '#22C55E' },
|
||||
today: { title: 'Heute', color: '#F59E0B' },
|
||||
overdue: { title: 'Überfällig', color: '#EF4444' },
|
||||
completed: { title: 'Erledigt', color: '#22C55E', icon: 'check' },
|
||||
today: { title: 'Heute', color: '#F59E0B', icon: 'calendar' },
|
||||
overdue: { title: 'Überfällig', color: '#EF4444', icon: 'warning' },
|
||||
all: { title: 'Alle Aufgaben', color: '#3B82F6' },
|
||||
'high-priority': { title: 'Hohe Priorität', color: '#EF4444' },
|
||||
'this-week': { title: 'Diese Woche', color: '#8B5CF6' },
|
||||
'high-priority': { title: 'Hohe Priorität', color: '#EF4444', icon: 'fire' },
|
||||
'this-week': { title: 'Diese Woche', color: '#8B5CF6', icon: 'calendar-dots' },
|
||||
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
|
||||
};
|
||||
|
||||
let pageMeta = $derived(PAGE_META[pageId] ?? { title: pageId, color: '#6B7280' });
|
||||
let displayTitle = $derived(customTitle ?? pageMeta.title);
|
||||
let displayColor = $derived(pageColor ?? pageMeta.color);
|
||||
let displayIcon = $derived(pageIcon ?? pageMeta.icon);
|
||||
let IconComponent = $derived(displayIcon ? ICON_MAP[displayIcon] : undefined);
|
||||
let isCustom = $derived(pageId.startsWith('custom-'));
|
||||
|
||||
// Filter tasks - either by custom filter config or preset page ID
|
||||
let filteredTasks = $derived.by(() => {
|
||||
const tasks = tasksCtx.value;
|
||||
const today = startOfDay(new Date());
|
||||
const weekEnd = addDays(today, 7);
|
||||
|
||||
// Custom filter-based pages
|
||||
if (filterConfig) {
|
||||
return tasks.filter((task) => {
|
||||
// Completed filter
|
||||
if (filterConfig.completed) {
|
||||
if (!task.isCompleted) return false;
|
||||
} else {
|
||||
if (task.isCompleted) return false;
|
||||
}
|
||||
|
||||
// Priority filter
|
||||
if (filterConfig.priorities?.length) {
|
||||
if (!filterConfig.priorities.includes(task.priority as any)) return false;
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
if (filterConfig.dateRange && filterConfig.dateRange !== 'any') {
|
||||
if (!task.dueDate) return false;
|
||||
const dueDate = startOfDay(new Date(task.dueDate));
|
||||
switch (filterConfig.dateRange) {
|
||||
case 'overdue':
|
||||
if (!isPast(dueDate) || isToday(dueDate)) return false;
|
||||
break;
|
||||
case 'today':
|
||||
if (!isToday(dueDate)) return false;
|
||||
break;
|
||||
case 'tomorrow':
|
||||
if (!isTomorrow(dueDate)) return false;
|
||||
break;
|
||||
case 'upcoming':
|
||||
if (isPast(dueDate) && !isToday(dueDate)) return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Preset page filtering (existing logic)
|
||||
switch (pageId) {
|
||||
case 'todo': {
|
||||
const recentCutoff = subHours(new Date(), 24);
|
||||
|
|
@ -150,8 +249,13 @@
|
|||
|
||||
let sheetWidth = $derived(PAGE_WIDTH_MAP[todoSettings.pageWidth] || PAGE_WIDTH_MAP.medium);
|
||||
|
||||
let showCompleted = $derived(filterConfig?.completed ?? false);
|
||||
let openTasks = $derived(
|
||||
pageId === 'todo' ? filteredTasks.filter((t) => !t.isCompleted) : filteredTasks
|
||||
pageId === 'todo'
|
||||
? filteredTasks.filter((t) => !t.isCompleted)
|
||||
: showCompleted
|
||||
? filteredTasks
|
||||
: filteredTasks
|
||||
);
|
||||
let recentlyCompleted = $derived(
|
||||
pageId === 'todo' ? filteredTasks.filter((t) => t.isCompleted) : []
|
||||
|
|
@ -160,7 +264,7 @@
|
|||
function formatCompletedTime(completedAt: string): string {
|
||||
const date = new Date(completedAt);
|
||||
const time = format(date, 'HH:mm');
|
||||
if (pageId === 'completed') {
|
||||
if (pageId === 'completed' || showCompleted) {
|
||||
const dateStr = format(date, 'dd.MM.');
|
||||
return $t('page.completedAtDateTime', { values: { date: dateStr, time } });
|
||||
}
|
||||
|
|
@ -173,29 +277,59 @@
|
|||
async function handleInlineCreate() {
|
||||
const title = newTaskTitle.trim();
|
||||
if (!title) return;
|
||||
const data: { title: string; dueDate?: string } = { title };
|
||||
if (pageId === 'today') {
|
||||
const data: Record<string, unknown> = { title };
|
||||
// Inherit filter context for new tasks
|
||||
if (pageId === 'today' || filterConfig?.dateRange === 'today') {
|
||||
data.dueDate = new Date().toISOString();
|
||||
} else if (pageId === 'this-week') {
|
||||
data.dueDate = new Date().toISOString();
|
||||
}
|
||||
await tasksStore.createTask(data);
|
||||
if (filterConfig?.priorities?.length === 1) {
|
||||
data.priority = filterConfig.priorities[0];
|
||||
}
|
||||
await tasksStore.createTask(
|
||||
data as { title: string; dueDate?: string; priority?: 'low' | 'medium' | 'high' | 'urgent' }
|
||||
);
|
||||
newTaskTitle = '';
|
||||
inputEl?.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="todo-page" class:maximized style="width: {maximized ? '100%' : sheetWidth}">
|
||||
<div
|
||||
class="todo-page"
|
||||
class:maximized
|
||||
class:editing={editMode}
|
||||
style="width: {maximized ? '100%' : sheetWidth}"
|
||||
>
|
||||
<div class="drag-handle-bar">
|
||||
<span class="drag-handle">
|
||||
<DotsSixVertical size={14} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Edit bar for custom pages -->
|
||||
{#if editMode && isCustom && customPageConfig && onUpdateConfig && onDelete}
|
||||
<PageEditBar
|
||||
config={customPageConfig}
|
||||
onUpdate={onUpdateConfig}
|
||||
{onMoveLeft}
|
||||
{onMoveRight}
|
||||
{onDelete}
|
||||
{isFirst}
|
||||
{isLast}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-header" ondragstart={(e) => e.preventDefault()}>
|
||||
<div class="header-left">
|
||||
<span class="color-dot" style="background-color: {pageMeta.color}"></span>
|
||||
{#if IconComponent}
|
||||
<span class="header-icon" style="color: {displayColor}">
|
||||
<IconComponent size={16} weight="fill" />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="color-dot" style="background-color: {displayColor}"></span>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<span
|
||||
bind:this={titleEl}
|
||||
|
|
@ -209,33 +343,40 @@
|
|||
<span class="task-count">{filteredTasks.length}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{#if onMinimize}
|
||||
<button class="header-btn" onclick={onMinimize} title="Minimieren">
|
||||
<Minus size={14} />
|
||||
{#if editMode && !isCustom && onDelete}
|
||||
<button class="header-btn delete-preset" onclick={onDelete} title="Seite entfernen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMaximize}
|
||||
<button
|
||||
class="header-btn"
|
||||
onclick={onMaximize}
|
||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||
>
|
||||
{#if maximized}
|
||||
<CornersIn size={14} />
|
||||
{:else}
|
||||
<CornersOut size={14} />
|
||||
{/if}
|
||||
{#if !editMode}
|
||||
{#if onMinimize}
|
||||
<button class="header-btn" onclick={onMinimize} title="Minimieren">
|
||||
<Minus size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if onMaximize}
|
||||
<button
|
||||
class="header-btn"
|
||||
onclick={onMaximize}
|
||||
title={maximized ? 'Verkleinern' : 'Maximieren'}
|
||||
>
|
||||
{#if maximized}
|
||||
<CornersIn size={14} />
|
||||
{:else}
|
||||
<CornersOut size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="header-btn" onclick={onClose} title="Seite schließen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<button class="header-btn" onclick={onClose} title="Seite schließen">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="page-body" ondragstart={(e) => e.preventDefault()}>
|
||||
{#if pageId === 'completed'}
|
||||
{#if pageId === 'completed' || showCompleted}
|
||||
{#each filteredTasks as task (task.id)}
|
||||
<div class="task-card-wrapper completed-task">
|
||||
<KanbanTaskCard
|
||||
|
|
@ -280,20 +421,22 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="inline-create">
|
||||
<span class="inline-create-icon">
|
||||
<Circle size={18} />
|
||||
</span>
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
bind:value={newTaskTitle}
|
||||
class="inline-create-input"
|
||||
placeholder={$t('page.newTaskPlaceholder')}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') handleInlineCreate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if !editMode}
|
||||
<div class="inline-create">
|
||||
<span class="inline-create-icon">
|
||||
<Circle size={18} />
|
||||
</span>
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
bind:value={newTaskTitle}
|
||||
class="inline-create-input"
|
||||
placeholder={$t('page.newTaskPlaceholder')}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') handleInlineCreate();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -318,6 +461,17 @@
|
|||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.todo-page.editing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.12),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
:global(.dark) .todo-page.editing {
|
||||
box-shadow:
|
||||
0 2px 12px rgba(139, 92, 246, 0.2),
|
||||
0 0 0 2px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.todo-page.maximized {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
|
@ -408,6 +562,12 @@
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
|
|
@ -470,6 +630,15 @@
|
|||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.delete-preset:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
:global(.dark) .delete-preset:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.page-body {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
* Page registers callbacks (restore/remove/togglePicker) and syncs its openPages.
|
||||
*/
|
||||
import type { MinimizedPage } from '@manacore/shared-ui';
|
||||
import type { PageConfig } from '$lib/stores/settings.svelte';
|
||||
|
||||
export const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
todo: { title: 'To Do', color: '#6B7280' },
|
||||
|
|
@ -17,13 +18,43 @@ export const PAGE_META: Record<string, { title: string; color: string }> = {
|
|||
'no-date': { title: 'Ohne Datum', color: '#6B7280' },
|
||||
};
|
||||
|
||||
/** Icon-to-color mapping for custom pages */
|
||||
const ICON_COLORS: Record<string, string> = {
|
||||
warning: '#EF4444',
|
||||
calendar: '#3B82F6',
|
||||
'calendar-dots': '#8B5CF6',
|
||||
check: '#22C55E',
|
||||
star: '#F59E0B',
|
||||
lightning: '#F97316',
|
||||
clock: '#6B7280',
|
||||
fire: '#EF4444',
|
||||
leaf: '#22C55E',
|
||||
heart: '#EC4899',
|
||||
};
|
||||
|
||||
/** Get display meta for a page, supporting both presets and custom pages */
|
||||
export function getPageMeta(
|
||||
pageId: string,
|
||||
customPages: PageConfig[]
|
||||
): { title: string; color: string } {
|
||||
if (PAGE_META[pageId]) return PAGE_META[pageId];
|
||||
const custom = customPages.find((p) => p.id === pageId);
|
||||
if (custom) {
|
||||
return {
|
||||
title: custom.label,
|
||||
color: ICON_COLORS[custom.icon ?? 'star'] ?? '#8B5CF6',
|
||||
};
|
||||
}
|
||||
return { title: pageId, color: '#6B7280' };
|
||||
}
|
||||
|
||||
export interface MinimizedPagesContext {
|
||||
/** Reactive list of minimized pages (read by layout for rendering) */
|
||||
readonly pages: MinimizedPage[];
|
||||
/** Whether there are any minimized pages */
|
||||
readonly hasPages: boolean;
|
||||
/** Sync open pages state from page component */
|
||||
sync(openPages: { id: string; minimized: boolean }[]): void;
|
||||
sync(openPages: { id: string; minimized: boolean }[], customPages?: PageConfig[]): void;
|
||||
/** Clear all pages (called on page unmount) */
|
||||
clear(): void;
|
||||
/** Restore a minimized page — delegates to registered callback */
|
||||
|
|
@ -59,11 +90,11 @@ export function createMinimizedPagesContext(): MinimizedPagesContext {
|
|||
get hasPages() {
|
||||
return pages.length > 0;
|
||||
},
|
||||
sync(openPages) {
|
||||
sync(openPages, customPages: PageConfig[] = []) {
|
||||
pages = openPages
|
||||
.filter((p) => p.minimized)
|
||||
.map((p) => {
|
||||
const meta = PAGE_META[p.id] ?? { title: p.id, color: '#6B7280' };
|
||||
const meta = getPageMeta(p.id, customPages);
|
||||
return { id: p.id, title: meta.title, color: meta.color };
|
||||
});
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue