feat(clock): improve UI across alarms, timers, pomodoro, and world clock pages

- Enhanced alarm page with preset suggestions and better layout
- Simplified timers page with cleaner controls
- Improved pomodoro with visual progress indicators
- World clock now shows interactive map with city markers
- Extended app.css with new utility classes and animations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-04 17:26:50 +01:00
parent f80b864ba8
commit 9dee75e06e
5 changed files with 750 additions and 622 deletions

View file

@ -157,7 +157,122 @@
color: hsl(var(--color-error));
}
/* Alarm Card */
/* Quick Create Form */
.quick-create {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: hsl(var(--color-surface));
border-radius: var(--radius-md);
padding: 0.5rem 0.75rem;
border: 1px solid hsl(var(--color-border));
}
.time-input-inline {
font-size: 1.5rem;
font-weight: 300;
width: 5rem;
padding: 0.25rem;
font-variant-numeric: tabular-nums;
background: transparent;
border: none;
color: hsl(var(--color-foreground));
}
.time-input-inline:focus {
outline: none;
}
.time-input-inline::-webkit-calendar-picker-indicator {
display: none;
}
.label-input {
flex: 1;
min-width: 0;
padding: 0.375rem 0.5rem;
font-size: 0.875rem;
background: transparent;
border: none;
color: hsl(var(--color-foreground));
}
.label-input:focus {
outline: none;
}
.label-input::placeholder {
color: hsl(var(--color-muted-foreground));
}
.day-selector-compact {
display: flex;
gap: 0.25rem;
padding: 0.5rem 0;
}
.day-selector-compact button {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid hsl(var(--color-border));
background: transparent;
color: hsl(var(--color-muted-foreground));
font-size: 0.7rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.day-selector-compact button:hover {
border-color: hsl(var(--color-primary));
color: hsl(var(--color-foreground));
}
.day-selector-compact button.active {
background-color: hsl(var(--color-primary));
border-color: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
}
/* Alarm Grid (responsive) */
.alarm-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 0.5rem;
}
/* Alarm Tile (Grid layout) */
.alarm-tile {
background-color: hsl(var(--color-surface));
border-radius: var(--radius-md);
padding: 0.5rem;
border: 1px solid hsl(var(--color-border));
transition: all var(--transition-base);
cursor: pointer;
opacity: 0.5;
}
.alarm-tile:hover {
background-color: hsl(var(--color-muted) / 0.5);
opacity: 0.7;
}
.alarm-tile:active {
background-color: hsl(var(--color-muted));
}
.alarm-tile.active {
opacity: 1;
border-color: hsl(var(--color-primary) / 0.3);
}
.alarm-tile.active:hover {
opacity: 1;
border-color: hsl(var(--color-primary) / 0.5);
}
/* Alarm Card (legacy, for edit form) */
.alarm-card {
background-color: hsl(var(--color-surface));
border-radius: var(--radius-lg);
@ -317,7 +432,105 @@
font-variant-numeric: tabular-nums;
}
/* Toggle Switch */
/* Time Input Small (for timer) */
.time-input-sm {
font-size: 1.5rem;
font-weight: 300;
text-align: center;
width: 3.5rem;
padding: 0.375rem;
font-variant-numeric: tabular-nums;
}
/* Time Input Large (for quick create) */
.time-input-large {
font-size: 2.5rem;
font-weight: 200;
text-align: center;
width: auto;
padding: 0.25rem 0.5rem;
font-variant-numeric: tabular-nums;
background: transparent;
border: none;
color: hsl(var(--color-foreground));
cursor: pointer;
}
.time-input-large:focus {
outline: none;
}
.time-input-large::-webkit-calendar-picker-indicator {
display: none;
}
/* Toggle Switch (iOS-style) */
.toggle-switch {
position: relative;
width: 44px;
height: 26px;
background-color: hsl(var(--color-muted));
border-radius: var(--radius-full);
cursor: pointer;
transition: background-color var(--transition-base);
border: none;
padding: 0;
flex-shrink: 0;
}
.toggle-switch.active {
background-color: hsl(var(--color-primary));
}
.toggle-switch .toggle-knob {
position: absolute;
top: 2px;
left: 2px;
width: 22px;
height: 22px;
background-color: white;
border-radius: 50%;
transition: transform var(--transition-base);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.toggle-switch.active .toggle-knob {
transform: translateX(18px);
}
/* Toggle Switch Small (for grid tiles) */
.toggle-switch-sm {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
background-color: hsl(var(--color-muted));
border-radius: var(--radius-full);
transition: background-color var(--transition-base);
flex-shrink: 0;
}
.toggle-switch-sm.active {
background-color: hsl(var(--color-primary));
}
.toggle-switch-sm .toggle-knob {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background-color: white;
border-radius: 50%;
transition: transform var(--transition-base);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.toggle-switch-sm.active .toggle-knob {
transform: translateX(16px);
}
/* Legacy toggle (deprecated) */
.toggle {
position: relative;
width: 44px;

View file

@ -1,81 +1,127 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
import { alarmsStore } from '$lib/stores/alarms.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import type { CreateAlarmInput } from '@clock/shared';
import { ALARM_SOUNDS } from '@clock/shared';
import type { CreateAlarmInput, Alarm } from '@clock/shared';
import { ALARM_SOUNDS, DEFAULT_ALARM_PRESETS } from '@clock/shared';
// Form state
let showForm = $state(false);
// Quick create form (inline)
let newTime = $state('07:00');
let newLabel = $state('');
let newRepeatDays = $state<number[]>([]);
let showOptions = $state(false);
// Edit modal state
let showEditModal = $state(false);
let editingId = $state<string | null>(null);
let formTime = $state('07:00');
let formLabel = $state('');
let formRepeatDays = $state<number[]>([]);
let formSound = $state('default');
let formSnoozeMinutes = $state(5);
let editTime = $state('07:00');
let editLabel = $state('');
let editRepeatDays = $state<number[]>([]);
let editSound = $state('default');
let editSnoozeMinutes = $state(5);
const dayNames = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
// Find existing alarm for a preset time
function findAlarmForPreset(presetTime: string): Alarm | undefined {
return alarmsStore.alarms.find((a) => a.time.slice(0, 5) === presetTime);
}
// Toggle a preset alarm
async function togglePreset(presetTime: string, presetLabel: string) {
const existingAlarm = findAlarmForPreset(presetTime);
if (existingAlarm) {
await alarmsStore.toggleAlarm(existingAlarm.id);
} else {
const result = await alarmsStore.createAlarm({
time: presetTime + ':00',
label: presetLabel,
enabled: true,
});
if (result.success) {
toast.success('Wecker erstellt');
} else {
toast.error(result.error || 'Fehler beim Erstellen');
}
}
}
// Quick create new alarm
async function handleQuickCreate() {
const result = await alarmsStore.createAlarm({
time: newTime + ':00',
label: newLabel || undefined,
repeatDays: newRepeatDays.length > 0 ? newRepeatDays : undefined,
enabled: true,
});
if (result.success) {
toast.success('Wecker erstellt');
// Reset form
newTime = '07:00';
newLabel = '';
newRepeatDays = [];
showOptions = false;
} else {
toast.error(result.error || 'Fehler beim Erstellen');
}
}
function toggleNewDay(day: number) {
if (newRepeatDays.includes(day)) {
newRepeatDays = newRepeatDays.filter((d) => d !== day);
} else {
newRepeatDays = [...newRepeatDays, day];
}
}
onMount(async () => {
if (authStore.isAuthenticated) {
await alarmsStore.fetchAlarms();
}
});
function openNewForm() {
editingId = null;
formTime = '07:00';
formLabel = '';
formRepeatDays = [];
formSound = 'default';
formSnoozeMinutes = 5;
showForm = true;
}
function openEditForm(alarm: any) {
function openEditModal(alarm: Alarm) {
editingId = alarm.id;
formTime = alarm.time.slice(0, 5); // HH:MM
formLabel = alarm.label || '';
formRepeatDays = alarm.repeatDays || [];
formSound = alarm.sound || 'default';
formSnoozeMinutes = alarm.snoozeMinutes || 5;
showForm = true;
editTime = alarm.time.slice(0, 5);
editLabel = alarm.label || '';
editRepeatDays = alarm.repeatDays || [];
editSound = alarm.sound || 'default';
editSnoozeMinutes = alarm.snoozeMinutes || 5;
showEditModal = true;
}
function closeForm() {
showForm = false;
function closeEditModal() {
showEditModal = false;
editingId = null;
}
function toggleDay(day: number) {
if (formRepeatDays.includes(day)) {
formRepeatDays = formRepeatDays.filter((d) => d !== day);
function toggleEditDay(day: number) {
if (editRepeatDays.includes(day)) {
editRepeatDays = editRepeatDays.filter((d) => d !== day);
} else {
formRepeatDays = [...formRepeatDays, day];
editRepeatDays = [...editRepeatDays, day];
}
}
async function handleSubmit() {
const input: CreateAlarmInput = {
time: formTime + ':00',
label: formLabel || undefined,
repeatDays: formRepeatDays.length > 0 ? formRepeatDays : undefined,
sound: formSound,
snoozeMinutes: formSnoozeMinutes,
};
async function handleEditSubmit() {
if (!editingId) return;
let result;
if (editingId) {
result = await alarmsStore.updateAlarm(editingId, input);
} else {
result = await alarmsStore.createAlarm(input);
}
const result = await alarmsStore.updateAlarm(editingId, {
time: editTime + ':00',
label: editLabel || undefined,
repeatDays: editRepeatDays.length > 0 ? editRepeatDays : undefined,
sound: editSound,
snoozeMinutes: editSnoozeMinutes,
});
if (result.success) {
toast.success(editingId ? 'Wecker aktualisiert' : 'Wecker erstellt');
closeForm();
toast.success('Wecker aktualisiert');
closeEditModal();
} else {
toast.error(result.error || 'Fehler beim Speichern');
}
@ -97,98 +143,134 @@
function getRepeatText(days: number[] | null) {
if (!days || days.length === 0) return 'Einmalig';
if (days.length === 7) return 'Täglich';
if (
days.length === 5 &&
days.includes(1) &&
days.includes(2) &&
days.includes(3) &&
days.includes(4) &&
days.includes(5)
)
return 'Wochentags';
if (days.length === 5 && [1, 2, 3, 4, 5].every((d) => days.includes(d))) return 'Wochentags';
if (days.length === 2 && days.includes(0) && days.includes(6)) return 'Am Wochenende';
return days.map((d) => dayNames[d]).join(', ');
}
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-foreground">{$_('alarm.title')}</h1>
<button class="btn btn-primary" onclick={openNewForm}>
+ {$_('alarm.add')}
<PageHeader title={$_('alarm.title')} size="md" centered />
<div class="space-y-4">
<!-- Quick Create Form -->
<div class="quick-create">
<input type="time" class="time-input-inline" bind:value={newTime} />
<input type="text" class="label-input" placeholder="Bezeichnung" bind:value={newLabel} />
<button
class="text-xs text-muted-foreground hover:text-foreground transition-colors px-2"
onclick={() => (showOptions = !showOptions)}
title="Wiederholung"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
class:text-primary={newRepeatDays.length > 0}
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clip-rule="evenodd"
/>
</svg>
</button>
<button class="btn btn-primary btn-sm" onclick={handleQuickCreate}> + </button>
</div>
<!-- Alarm List -->
{#if showOptions}
<div class="day-selector-compact">
{#each dayNames as day, i}
<button
type="button"
class:active={newRepeatDays.includes(i)}
onclick={() => toggleNewDay(i)}
>
{day}
</button>
{/each}
</div>
{/if}
<!-- Loading State -->
{#if alarmsStore.loading}
<div class="flex justify-center py-12">
<div
class="h-8 w-8 animate-spin rounded-full border-4 border-primary border-r-transparent"
></div>
</div>
{:else if alarmsStore.alarms.length === 0}
<div class="card py-12 text-center">
<p class="text-lg text-muted-foreground">{$_('alarm.noAlarms')}</p>
<button class="btn btn-primary mt-4" onclick={openNewForm}>
{$_('alarm.add')}
</button>
</div>
{:else}
<div class="space-y-3">
{#each alarmsStore.alarms as alarm (alarm.id)}
<div class="alarm-card" class:disabled={!alarm.enabled}>
<div class="flex items-center justify-between">
<div class="flex-1">
<button class="text-left w-full" onclick={() => openEditForm(alarm)}>
<div class="text-3xl font-light text-foreground">
{alarm.time.slice(0, 5)}
</div>
{#if alarm.label}
<p class="mt-1 text-sm font-medium text-foreground">{alarm.label}</p>
{/if}
<p class="mt-1 text-sm text-muted-foreground">
{getRepeatText(alarm.repeatDays)}
</p>
</button>
</div>
<div class="flex items-center gap-4">
<button
class="text-muted-foreground hover:text-error"
onclick={() => handleDelete(alarm.id)}
>
🗑
</button>
<button
class="toggle"
class:active={alarm.enabled}
onclick={() => handleToggle(alarm.id)}
></button>
</div>
<!-- Default Alarm Presets (Grid) -->
<div class="alarm-grid">
{#each DEFAULT_ALARM_PRESETS as preset}
{@const existingAlarm = findAlarmForPreset(preset.time)}
{@const isActive = existingAlarm?.enabled ?? false}
<div
class="alarm-tile"
class:active={isActive}
role="button"
tabindex="0"
onclick={() => togglePreset(preset.time, preset.label)}
onkeydown={(e) => e.key === 'Enter' && togglePreset(preset.time, preset.label)}
>
<div class="text-xl font-light text-foreground tabular-nums text-center">
{preset.time}
</div>
<div class="text-[10px] text-muted-foreground text-center truncate mt-0.5">
{existingAlarm?.label || preset.label}
</div>
</div>
{/each}
</div>
<!-- Custom Alarms (Grid) -->
{@const customAlarms = alarmsStore.alarms.filter(
(a) => !DEFAULT_ALARM_PRESETS.some((p) => p.time === a.time.slice(0, 5))
)}
{#if customAlarms.length > 0}
<div class="mt-4">
<h2 class="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
{$_('alarm.custom')}
</h2>
<div class="alarm-grid">
{#each customAlarms as alarm (alarm.id)}
<div
class="alarm-tile"
class:active={alarm.enabled}
role="button"
tabindex="0"
onclick={() => handleToggle(alarm.id)}
onkeydown={(e) => e.key === 'Enter' && handleToggle(alarm.id)}
>
<div class="text-xl font-light text-foreground tabular-nums text-center">
{alarm.time.slice(0, 5)}
</div>
<div class="text-[10px] text-muted-foreground text-center truncate mt-0.5">
{alarm.label || getRepeatText(alarm.repeatDays)}
</div>
</div>
{/each}
</div>
</div>
{/if}
{/if}
<!-- Form Modal -->
{#if showForm}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<!-- Edit Modal -->
{#if showEditModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div class="card w-full max-w-md">
<h2 class="mb-4 text-xl font-semibold">
{editingId ? $_('alarm.edit') : $_('alarm.add')}
</h2>
<h2 class="mb-4 text-xl font-semibold">{$_('alarm.edit')}</h2>
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
handleEditSubmit();
}}
>
<!-- Time -->
<div class="mb-4">
<label class="mb-1 block text-sm font-medium">{$_('alarm.time')}</label>
<input type="time" class="input time-input" bind:value={formTime} />
<input type="time" class="input time-input" bind:value={editTime} />
</div>
<!-- Label -->
@ -198,7 +280,7 @@
type="text"
class="input"
placeholder="Arbeit, Sport, etc."
bind:value={formLabel}
bind:value={editLabel}
/>
</div>
@ -209,8 +291,8 @@
{#each dayNames as day, i}
<button
type="button"
class:active={formRepeatDays.includes(i)}
onclick={() => toggleDay(i)}
class:active={editRepeatDays.includes(i)}
onclick={() => toggleEditDay(i)}
>
{day}
</button>
@ -221,7 +303,7 @@
<!-- Sound -->
<div class="mb-4">
<label class="mb-1 block text-sm font-medium">{$_('alarm.sound')}</label>
<select class="input" bind:value={formSound}>
<select class="input" bind:value={editSound}>
{#each ALARM_SOUNDS as sound}
<option value={sound.id}>{sound.nameDE}</option>
{/each}
@ -231,7 +313,7 @@
<!-- Snooze -->
<div class="mb-6">
<label class="mb-1 block text-sm font-medium">{$_('alarm.snooze')}</label>
<select class="input" bind:value={formSnoozeMinutes}>
<select class="input" bind:value={editSnoozeMinutes}>
<option value={5}>5 Minuten</option>
<option value={10}>10 Minuten</option>
<option value={15}>15 Minuten</option>
@ -241,7 +323,7 @@
<!-- Actions -->
<div class="flex gap-3">
<button type="button" class="btn btn-secondary flex-1" onclick={closeForm}>
<button type="button" class="btn btn-secondary flex-1" onclick={closeEditModal}>
{$_('common.cancel')}
</button>
<button type="submit" class="btn btn-primary flex-1">

View file

@ -1,33 +1,32 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
import { pomodoroStore } from '$lib/stores/pomodoro.svelte';
import { POMODORO_PRESETS } from '@clock/shared';
// SVG circle properties
const radius = 120;
const radius = 90;
const circumference = 2 * Math.PI * radius;
let strokeDashoffset = $derived(circumference - (pomodoroStore.progress / 100) * circumference);
let phaseLabel = $derived(
{
work: $_('pomodoro.work'),
break: $_('pomodoro.break'),
longBreak: $_('pomodoro.longBreak'),
work: 'Arbeit',
break: 'Pause',
longBreak: 'Lange Pause',
}[pomodoroStore.phase]
);
let phaseColor = $derived(
{
work: 'hsl(var(--color-primary))',
break: 'hsl(var(--color-success))',
longBreak: 'hsl(var(--color-info))',
break: 'hsl(142, 71%, 45%)',
longBreak: 'hsl(199, 89%, 48%)',
}[pomodoroStore.phase]
);
onMount(() => {
// Request notification permission
pomodoroStore.requestNotificationPermission();
});
@ -41,87 +40,44 @@
}
</script>
<div class="flex flex-col items-center space-y-8">
<!-- Header -->
<h1 class="text-2xl font-bold text-foreground">{$_('pomodoro.title')}</h1>
<PageHeader title={$_('pomodoro.title')} size="md" centered />
<!-- Phase indicator -->
<div class="text-center">
<span
class="inline-block rounded-full px-4 py-1 text-sm font-medium"
style="background-color: {phaseColor}; color: white;"
>
{phaseLabel}
</span>
</div>
<!-- Progress Ring -->
<div class="relative">
<svg width="280" height="280" class="-rotate-90">
<!-- Background circle -->
<div class="pomodoro-container">
<!-- Progress Ring with Time -->
<div class="pomodoro-ring-wrapper">
<svg width="200" height="200" class="-rotate-90">
<circle
cx="140"
cy="140"
cx="100"
cy="100"
r={radius}
fill="none"
stroke="hsl(var(--color-muted))"
stroke-width="8"
stroke-width="6"
/>
<!-- Progress circle -->
<circle
cx="140"
cy="140"
cx="100"
cy="100"
r={radius}
fill="none"
stroke={phaseColor}
stroke-width="8"
stroke-width="6"
stroke-linecap="round"
stroke-dasharray={circumference}
stroke-dashoffset={strokeDashoffset}
class="transition-all duration-1000 ease-linear"
/>
</svg>
<!-- Time display -->
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="digital-clock text-5xl font-light text-foreground">
{pomodoroStore.formattedTime}
</span>
<span class="mt-2 text-sm text-muted-foreground">
{$_('pomodoro.sessionsCompleted', {
values: {
count: pomodoroStore.completedSessions,
total: pomodoroStore.sessionsBeforeLongBreak,
},
})}
</span>
<div class="pomodoro-time">
<span class="text-4xl font-light tabular-nums">{pomodoroStore.formattedTime}</span>
<span class="text-xs text-muted-foreground mt-1">{phaseLabel}</span>
</div>
</div>
<!-- Controls -->
<div class="flex gap-4">
{#if pomodoroStore.isRunning}
<button class="btn btn-secondary btn-xl" onclick={() => pomodoroStore.pause()}>
{$_('pomodoro.pause')}
</button>
{:else}
<button class="btn btn-primary btn-xl" onclick={() => pomodoroStore.start()}>
{$_('pomodoro.start')}
</button>
{/if}
<button class="btn btn-ghost btn-xl" onclick={() => pomodoroStore.skip()}>
{$_('pomodoro.skip')}
</button>
<button class="btn btn-ghost btn-xl" onclick={() => pomodoroStore.reset()}>
{$_('pomodoro.reset')}
</button>
</div>
<!-- Sessions Progress -->
<div class="flex gap-2">
<!-- Session dots -->
<div class="flex justify-center gap-1.5 mb-4">
{#each Array(pomodoroStore.sessionsBeforeLongBreak) as _, i}
<div
class="h-3 w-3 rounded-full transition-colors"
class="h-2 w-2 rounded-full transition-colors"
class:bg-primary={i <
pomodoroStore.completedSessions % pomodoroStore.sessionsBeforeLongBreak}
class:bg-muted={i >=
@ -130,43 +86,51 @@
{/each}
</div>
<!-- Presets -->
<div class="card w-full max-w-md">
<h3 class="mb-3 text-sm font-medium text-muted-foreground">{$_('timer.presets')}</h3>
<div class="grid gap-2 sm:grid-cols-3">
{#each POMODORO_PRESETS as preset}
<button class="btn btn-secondary btn-sm text-left" onclick={() => loadPreset(preset)}>
<div>
<div class="font-medium">{preset.nameDE}</div>
<div class="text-xs text-muted-foreground">
{preset.workDuration / 60}:{preset.breakDuration / 60} min
</div>
</div>
</button>
{/each}
</div>
<!-- Controls -->
<div class="flex justify-center gap-2 mb-6">
{#if pomodoroStore.isRunning}
<button class="btn btn-secondary" onclick={() => pomodoroStore.pause()}> Pause </button>
{:else}
<button class="btn btn-primary" onclick={() => pomodoroStore.start()}> Start </button>
{/if}
<button class="btn btn-ghost" onclick={() => pomodoroStore.skip()}> Skip </button>
<button class="btn btn-ghost" onclick={() => pomodoroStore.reset()}> Reset </button>
</div>
<!-- Current Settings -->
<div class="card w-full max-w-md">
<h3 class="mb-3 text-sm font-medium text-muted-foreground">Aktuelle Einstellungen</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-muted-foreground">{$_('pomodoro.settings.workDuration')}:</span>
<span class="ml-1 font-medium">{pomodoroStore.settings.workDuration / 60} min</span>
</div>
<div>
<span class="text-muted-foreground">{$_('pomodoro.settings.breakDuration')}:</span>
<span class="ml-1 font-medium">{pomodoroStore.settings.breakDuration / 60} min</span>
</div>
<div>
<span class="text-muted-foreground">{$_('pomodoro.settings.longBreakDuration')}:</span>
<span class="ml-1 font-medium">{pomodoroStore.settings.longBreakDuration / 60} min</span>
</div>
<div>
<span class="text-muted-foreground">Sitzungen:</span>
<span class="ml-1 font-medium">{pomodoroStore.settings.sessionsBeforeLongBreak}</span>
</div>
</div>
<!-- Presets -->
<div class="grid grid-cols-3 gap-1.5">
{#each POMODORO_PRESETS as preset}
<button class="alarm-tile text-center" onclick={() => loadPreset(preset)}>
<span class="text-sm font-medium">{preset.nameDE}</span>
<span class="text-[10px] text-muted-foreground block">
{preset.workDuration / 60}/{preset.breakDuration / 60} min
</span>
</button>
{/each}
</div>
</div>
<style>
.pomodoro-container {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 1rem;
}
.pomodoro-ring-wrapper {
position: relative;
width: 200px;
height: 200px;
margin-bottom: 1rem;
}
.pomodoro-time {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>

View file

@ -2,19 +2,18 @@
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { browser } from '$app/environment';
import { PageHeader } from '@manacore/shared-ui';
import { timersStore } from '$lib/stores/timers.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { QUICK_TIMER_PRESETS, formatDuration } from '@clock/shared';
// Form state
let showForm = $state(false);
let formHours = $state(0);
// Form state (inline on page)
let formMinutes = $state(5);
let formSeconds = $state(0);
let formLabel = $state('');
// Local timers (for quick timers without backend)
// Local timers
interface LocalTimer {
id: string;
label: string;
@ -24,56 +23,20 @@
createdAt: Date;
}
let localTimers = $state<LocalTimer[]>([]);
// Recently used presets (stored in localStorage)
let recentPresets = $state<{ label: string; seconds: number }[]>([]);
const RECENT_STORAGE_KEY = 'clock-recent-timers';
const MAX_RECENT = 5;
// Local countdown intervals
let intervals: Map<string, ReturnType<typeof setInterval>> = new Map();
// Combined timers (backend + local)
let allTimers = $derived([...timersStore.timers, ...localTimers]);
onMount(async () => {
// Load recent presets from localStorage
if (browser) {
const saved = localStorage.getItem(RECENT_STORAGE_KEY);
if (saved) {
try {
recentPresets = JSON.parse(saved);
} catch {
recentPresets = [];
}
}
}
// Fetch backend timers if authenticated
if (authStore.isAuthenticated) {
await timersStore.fetchTimers();
}
});
onDestroy(() => {
// Clear all intervals
intervals.forEach((interval) => clearInterval(interval));
});
function saveRecentPreset(preset: { label: string; seconds: number }) {
// Add to recent, removing duplicates
recentPresets = [
preset,
...recentPresets.filter((p) => p.seconds !== preset.seconds),
].slice(0, MAX_RECENT);
if (browser) {
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(recentPresets));
}
}
function startLocalCountdown(timerId: string, isLocal: boolean = false) {
// Clear existing interval if any
if (intervals.has(timerId)) {
clearInterval(intervals.get(timerId));
}
@ -90,7 +53,11 @@
const newRemaining = Math.max(0, timer.remainingSeconds - 1);
localTimers = localTimers.map((t) =>
t.id === timerId
? { ...t, remainingSeconds: newRemaining, status: newRemaining === 0 ? 'finished' : 'running' }
? {
...t,
remainingSeconds: newRemaining,
status: newRemaining === 0 ? 'finished' : 'running',
}
: t
);
@ -98,7 +65,6 @@
clearInterval(interval);
intervals.delete(timerId);
toast.success($_('timer.finished'));
// Play notification sound
if (browser && 'Notification' in window && Notification.permission === 'granted') {
new Notification('Timer', { body: 'Timer abgelaufen!' });
}
@ -125,69 +91,30 @@
intervals.set(timerId, interval);
}
function openForm() {
formHours = 0;
formMinutes = 5;
formSeconds = 0;
formLabel = '';
showForm = true;
}
function closeForm() {
showForm = false;
}
async function createTimer() {
const durationSeconds = formHours * 3600 + formMinutes * 60 + formSeconds;
function createAndStartTimer() {
const durationSeconds = formMinutes * 60 + formSeconds;
if (durationSeconds <= 0) {
toast.error('Bitte eine gültige Zeit eingeben');
return;
}
if (authStore.isAuthenticated) {
const result = await timersStore.createTimer({
durationSeconds,
label: formLabel || undefined,
});
if (result.success) {
toast.success('Timer erstellt');
closeForm();
} else {
toast.error(result.error || 'Fehler beim Erstellen');
}
} else {
// Create local timer
const newTimer: LocalTimer = {
id: crypto.randomUUID(),
label: formLabel || formatDuration(durationSeconds),
durationSeconds,
remainingSeconds: durationSeconds,
status: 'idle',
createdAt: new Date(),
};
localTimers = [...localTimers, newTimer];
toast.success('Timer erstellt');
closeForm();
}
}
function createQuickTimer(seconds: number, label: string) {
// Save to recent
saveRecentPreset({ label, seconds });
// Create local timer and start immediately
const newTimer: LocalTimer = {
id: crypto.randomUUID(),
label: label,
durationSeconds: seconds,
remainingSeconds: seconds,
label: formLabel || formatDuration(durationSeconds),
durationSeconds,
remainingSeconds: durationSeconds,
status: 'running',
createdAt: new Date(),
};
localTimers = [...localTimers, newTimer];
startLocalCountdown(newTimer.id, true);
toast.success(`Timer ${label} gestartet`);
toast.success('Timer gestartet');
formLabel = '';
}
function setPreset(seconds: number) {
formMinutes = Math.floor(seconds / 60);
formSeconds = seconds % 60;
}
async function handleStart(id: string, isLocal: boolean) {
@ -198,12 +125,7 @@
startLocalCountdown(id, true);
} else {
const result = await timersStore.startTimer(id);
if (result.success) {
const timer = timersStore.timers.find((t) => t.id === id);
if (timer) {
startLocalCountdown(id, false);
}
}
if (result.success) startLocalCountdown(id, false);
}
}
@ -212,11 +134,8 @@
clearInterval(intervals.get(id));
intervals.delete(id);
}
if (isLocal) {
localTimers = localTimers.map((t) =>
t.id === id ? { ...t, status: 'paused' as const } : t
);
localTimers = localTimers.map((t) => (t.id === id ? { ...t, status: 'paused' as const } : t));
} else {
await timersStore.pauseTimer(id);
}
@ -227,7 +146,6 @@
clearInterval(intervals.get(id));
intervals.delete(id);
}
if (isLocal) {
localTimers = localTimers.map((t) =>
t.id === id ? { ...t, remainingSeconds: t.durationSeconds, status: 'idle' as const } : t
@ -242,15 +160,10 @@
clearInterval(intervals.get(id));
intervals.delete(id);
}
if (isLocal) {
localTimers = localTimers.filter((t) => t.id !== id);
toast.success('Timer gelöscht');
} else {
const result = await timersStore.deleteTimer(id);
if (result.success) {
toast.success('Timer gelöscht');
}
await timersStore.deleteTimer(id);
}
}
@ -267,189 +180,120 @@
function isLocalTimer(timer: any): boolean {
return localTimers.some((t) => t.id === timer.id);
}
// Preset icons based on duration
function getPresetIcon(seconds: number): string {
if (seconds <= 60) return '⚡';
if (seconds <= 300) return '☕';
if (seconds <= 900) return '📝';
if (seconds <= 1800) return '💪';
if (seconds <= 2700) return '🎯';
return '🏃';
}
</script>
<div class="mx-auto max-w-4xl space-y-8 pb-8">
<!-- Header -->
<div class="text-center">
<h1 class="text-3xl font-bold text-foreground">{$_('timer.title')}</h1>
<p class="mt-2 text-muted-foreground">Schnelle Timer für jeden Anlass</p>
</div>
<PageHeader title={$_('timer.title')} size="md" centered />
<!-- Quick Timer Presets - Hero Section -->
<div class="card bg-gradient-to-br from-primary/10 to-primary/5 p-8">
<h2 class="mb-6 text-center text-lg font-semibold text-foreground">
{$_('timer.presets')}
</h2>
<div class="grid grid-cols-4 gap-4 sm:grid-cols-4 md:grid-cols-8">
{#each QUICK_TIMER_PRESETS as preset}
<button
class="group flex flex-col items-center gap-2 rounded-2xl bg-background p-4 shadow-sm transition-all hover:scale-105 hover:shadow-md active:scale-95"
onclick={() => createQuickTimer(preset.seconds, preset.label)}
>
<span class="text-2xl transition-transform group-hover:scale-110">
{getPresetIcon(preset.seconds)}
</span>
<span class="text-sm font-medium text-foreground">{preset.label}</span>
</button>
{/each}
<div class="space-y-4">
<!-- Quick Create Form -->
<div class="quick-create">
<div class="flex items-center gap-1">
<input
type="number"
class="time-input-inline w-12 text-center"
min="0"
max="99"
bind:value={formMinutes}
/>
<span class="text-muted-foreground">:</span>
<input
type="number"
class="time-input-inline w-12 text-center"
min="0"
max="59"
bind:value={formSeconds}
/>
</div>
<input type="text" class="label-input" placeholder="Bezeichnung" bind:value={formLabel} />
<button class="btn btn-primary btn-sm" onclick={createAndStartTimer}> Start </button>
</div>
<!-- Recently Used Section -->
{#if recentPresets.length > 0}
<div class="card">
<div class="mb-4 flex items-center justify-between">
<h3 class="text-lg font-semibold text-foreground">Zuletzt verwendet</h3>
<button
class="text-sm text-muted-foreground hover:text-foreground"
onclick={() => {
recentPresets = [];
if (browser) localStorage.removeItem(RECENT_STORAGE_KEY);
}}
>
Löschen
</button>
</div>
<div class="flex flex-wrap gap-3">
{#each recentPresets as preset}
<button
class="flex items-center gap-2 rounded-full bg-muted px-4 py-2 text-sm font-medium transition-all hover:bg-primary hover:text-primary-foreground"
onclick={() => createQuickTimer(preset.seconds, preset.label)}
>
<span class="text-lg">{getPresetIcon(preset.seconds)}</span>
{preset.label}
</button>
{/each}
</div>
</div>
{/if}
<!-- Custom Timer Button -->
<div class="flex justify-center">
<button
class="btn btn-primary btn-lg flex items-center gap-2 px-8"
onclick={openForm}
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
{$_('timer.add')}
</button>
<!-- Quick Presets -->
<div class="grid grid-cols-4 sm:grid-cols-8 gap-1.5">
{#each QUICK_TIMER_PRESETS as preset}
<button class="alarm-tile text-center" onclick={() => setPreset(preset.seconds)}>
<span class="text-lg font-light tabular-nums">{preset.label}</span>
</button>
{/each}
</div>
<!-- Active Timers -->
<!-- Loading State -->
{#if timersStore.loading}
<div class="flex justify-center py-12">
<div class="h-10 w-10 animate-spin rounded-full border-4 border-primary border-r-transparent"></div>
<div class="flex justify-center py-8">
<div
class="h-6 w-6 animate-spin rounded-full border-2 border-primary border-r-transparent"
></div>
</div>
{:else if allTimers.length === 0}
<div class="card bg-muted/30 py-16 text-center">
<div class="mb-4 text-5xl opacity-50">⏱️</div>
<p class="text-lg text-muted-foreground">{$_('timer.noTimers')}</p>
<p class="mt-2 text-sm text-muted-foreground">
Klicke auf einen Preset oben oder erstelle einen eigenen Timer
</p>
</div>
{:else}
<div class="space-y-4">
<h3 class="text-lg font-semibold text-foreground">
Aktive Timer ({allTimers.length})
</h3>
<div class="grid gap-4 sm:grid-cols-2">
{:else if allTimers.length > 0}
<!-- Active Timers -->
<div>
<h2 class="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
Aktiv ({allTimers.length})
</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
{#each allTimers as timer (timer.id)}
{@const isLocal = isLocalTimer(timer)}
<div
class="card relative overflow-hidden transition-all {timer.status === 'running' ? 'ring-2 ring-primary' : ''} {timer.status === 'finished' ? 'bg-green-500/5' : ''}"
>
<!-- Progress background -->
<div
class="absolute inset-0 bg-primary/10 transition-all duration-1000"
style="width: {getProgress(timer)}%"
></div>
<div class="relative">
<!-- Label -->
<div class="mb-3 flex items-center justify-between">
<span class="text-sm font-medium text-muted-foreground">
{timer.label || 'Timer'}
</span>
<span
class="rounded-full px-2 py-0.5 text-xs font-medium"
class:bg-primary={timer.status === 'running'}
class:text-primary-foreground={timer.status === 'running'}
class:bg-muted={timer.status !== 'running'}
<div class="alarm-tile" class:active={timer.status === 'running'}>
<div class="flex items-start justify-between mb-1">
<span
class="text-xl font-light tabular-nums"
class:text-primary={timer.status === 'running'}
class:text-green-500={timer.status === 'finished'}
>
{getTimerDisplay(timer)}
</span>
<button
class="text-muted-foreground hover:text-error p-0.5 -mr-1"
onclick={(e) => {
e.stopPropagation();
handleDelete(timer.id, isLocal);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5"
viewBox="0 0 20 20"
fill="currentColor"
>
{timer.status === 'running' ? 'Läuft' : timer.status === 'paused' ? 'Pausiert' : timer.status === 'finished' ? 'Fertig' : 'Bereit'}
</span>
</div>
<!-- Time Display -->
<div class="mb-4 text-center">
<span
class="font-mono text-5xl font-light tracking-tight {timer.status === 'running' ? 'text-primary' : ''} {timer.status === 'finished' ? 'text-green-500' : ''}"
>
{getTimerDisplay(timer)}
</span>
</div>
<!-- Progress bar -->
<div class="mb-4 h-1.5 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full transition-all duration-1000 {timer.status === 'finished' ? 'bg-green-500' : 'bg-primary'}"
style="width: {getProgress(timer)}%"
></div>
</div>
<!-- Controls -->
<div class="flex items-center gap-2">
{#if timer.status === 'running'}
<button
class="btn btn-secondary flex-1"
onclick={() => handlePause(timer.id, isLocal)}
>
⏸️ {$_('timer.pause')}
</button>
{:else if timer.status === 'finished'}
<button
class="btn btn-primary flex-1"
onclick={() => handleReset(timer.id, isLocal)}
>
🔄 Neu starten
</button>
{:else}
<button
class="btn btn-primary flex-1"
onclick={() => handleStart(timer.id, isLocal)}
>
▶️ {$_('timer.start')}
</button>
{/if}
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
<div class="text-[10px] text-muted-foreground truncate mb-2">{timer.label}</div>
<div class="h-1 bg-muted rounded-full overflow-hidden mb-2">
<div
class="h-full rounded-full transition-all duration-1000"
class:bg-primary={timer.status !== 'finished'}
class:bg-green-500={timer.status === 'finished'}
style="width: {getProgress(timer)}%"
></div>
</div>
<div class="flex gap-1">
{#if timer.status === 'running'}
<button
class="btn btn-ghost"
onclick={() => handleReset(timer.id, isLocal)}
title="Zurücksetzen"
class="btn btn-secondary btn-sm flex-1 text-xs"
onclick={() => handlePause(timer.id, isLocal)}
>
Pause
</button>
{:else}
<button
class="btn btn-ghost text-destructive hover:bg-destructive/10"
onclick={() => handleDelete(timer.id, isLocal)}
title="Löschen"
class="btn btn-primary btn-sm flex-1 text-xs"
onclick={() => handleStart(timer.id, isLocal)}
>
🗑️
{timer.status === 'finished' ? 'Neu' : 'Start'}
</button>
</div>
{/if}
<button
class="btn btn-ghost btn-sm text-xs"
onclick={() => handleReset(timer.id, isLocal)}
>
Reset
</button>
</div>
</div>
{/each}
@ -457,117 +301,3 @@
</div>
{/if}
</div>
<!-- Form Modal -->
{#if showForm}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div class="card w-full max-w-md animate-in fade-in zoom-in-95">
<div class="mb-6 flex items-center justify-between">
<h2 class="text-xl font-semibold">{$_('timer.add')}</h2>
<button class="btn btn-ghost btn-sm" onclick={closeForm}>✕</button>
</div>
<form
onsubmit={(e) => {
e.preventDefault();
createTimer();
}}
>
<!-- Duration -->
<div class="mb-6">
<label class="mb-3 block text-sm font-medium">{$_('timer.duration')}</label>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="mb-1.5 block text-xs text-muted-foreground text-center">{$_('timer.hours')}</label>
<input
type="number"
class="input text-center text-2xl font-light"
min="0"
max="99"
bind:value={formHours}
/>
</div>
<div>
<label class="mb-1.5 block text-xs text-muted-foreground text-center">{$_('timer.minutes')}</label>
<input
type="number"
class="input text-center text-2xl font-light"
min="0"
max="59"
bind:value={formMinutes}
/>
</div>
<div>
<label class="mb-1.5 block text-xs text-muted-foreground text-center">{$_('timer.seconds')}</label>
<input
type="number"
class="input text-center text-2xl font-light"
min="0"
max="59"
bind:value={formSeconds}
/>
</div>
</div>
</div>
<!-- Quick presets in modal -->
<div class="mb-6">
<label class="mb-2 block text-xs text-muted-foreground">Schnellauswahl</label>
<div class="flex flex-wrap gap-2">
{#each [1, 3, 5, 10, 15, 30] as mins}
<button
type="button"
class="rounded-full bg-muted px-3 py-1 text-sm hover:bg-primary hover:text-primary-foreground"
onclick={() => {
formHours = 0;
formMinutes = mins;
formSeconds = 0;
}}
>
{mins} min
</button>
{/each}
</div>
</div>
<!-- Label -->
<div class="mb-6">
<label class="mb-1.5 block text-sm font-medium">{$_('timer.label')}</label>
<input
type="text"
class="input"
placeholder="z.B. Tee kochen, Pause, Meeting..."
bind:value={formLabel}
/>
</div>
<!-- Actions -->
<div class="flex gap-3">
<button type="button" class="btn btn-secondary flex-1" onclick={closeForm}>
{$_('common.cancel')}
</button>
<button type="submit" class="btn btn-primary flex-1">
Timer erstellen
</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.animate-in {
animation: animate-in 0.2s ease-out;
}
@keyframes animate-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>

View file

@ -1,16 +1,32 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { PageHeader } from '@manacore/shared-ui';
import { worldClocksStore } from '$lib/stores/world-clocks.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { toast } from '$lib/stores/toast';
import { POPULAR_TIMEZONES } from '@clock/shared';
import WorldMap from '$lib/components/WorldMap.svelte';
// State
let showAddModal = $state(false);
let searchQuery = $state('');
let currentTime = $state(new Date());
let interval: ReturnType<typeof setInterval> | null = null;
let showMap = $state(true);
// Selected city timezones for map highlighting
let selectedTimezones = $derived(worldClocksStore.worldClocks.map((wc) => wc.timezone));
// Handle map city click
function handleMapCityClick(timezone: string, cityName: string) {
const alreadyAdded = worldClocksStore.worldClocks.some((wc) => wc.timezone === timezone);
if (alreadyAdded) {
toast.info(`${cityName} ist bereits hinzugefügt`);
} else {
addCity(timezone, cityName);
}
}
// Filtered timezones based on search
let filteredTimezones = $derived(
@ -139,14 +155,53 @@
}
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-foreground">{$_('worldClock.title')}</h1>
<button class="btn btn-primary" onclick={openAddModal}>
+ {$_('worldClock.add')}
</button>
</div>
<PageHeader title={$_('worldClock.title')} size="md" centered>
{#snippet actions()}
<div class="flex items-center gap-2">
<button
class="btn btn-ghost btn-sm px-2"
onclick={() => (showMap = !showMap)}
title={showMap ? 'Karte ausblenden' : 'Karte anzeigen'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>
</button>
<button class="btn btn-primary btn-sm" onclick={openAddModal}>
+ {$_('worldClock.add')}
</button>
</div>
{/snippet}
</PageHeader>
<div class="world-clock-page">
<!-- World Map (Full Width) -->
{#if showMap}
<div class="map-section">
<div class="map-container">
<WorldMap
selectedCities={selectedTimezones}
onCityClick={handleMapCityClick}
{currentTime}
/>
</div>
<p class="text-center text-xs text-muted-foreground py-2">
Klicke auf eine Stadt um sie hinzuzufügen
</p>
</div>
{/if}
<!-- World Clock List -->
{#if worldClocksStore.loading}
@ -169,15 +224,26 @@
<div class="world-clock-card relative">
<!-- Delete button -->
<button
class="absolute right-3 top-3 text-muted-foreground hover:text-error"
class="absolute right-3 top-3 text-muted-foreground hover:text-error p-0.5"
onclick={() => removeCity(clock.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
<!-- Day/Night indicator -->
<div class="mb-2 flex items-center gap-2">
<span class="text-xl">{isDay ? '☀️' : '🌙'}</span>
<span class="text-xs text-muted-foreground">{isDay ? 'Tag' : 'Nacht'}</span>
<span class="city-name">{clock.cityName}</span>
</div>
@ -206,8 +272,19 @@
<div class="card w-full max-w-md max-h-[80vh] flex flex-col">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">{$_('worldClock.add')}</h2>
<button class="text-muted-foreground hover:text-foreground" onclick={closeAddModal}>
<button class="text-muted-foreground hover:text-foreground p-0.5" onclick={closeAddModal}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
@ -252,3 +329,65 @@
</div>
{/if}
</div>
<style>
.world-clock-page {
display: flex;
flex-direction: column;
min-height: calc(100vh - 180px);
}
.map-section {
display: flex;
flex-direction: column;
margin: 0 -1rem 1rem -1rem;
background: hsl(var(--color-card));
border-bottom: 1px solid hsl(var(--color-border));
}
.map-container {
width: 100%;
max-height: 50vh;
overflow: hidden;
}
.map-container :global(.world-map-container) {
border-radius: 0;
box-shadow: none;
}
@media (min-width: 768px) {
.map-section {
margin: 0 -1.5rem 1.5rem -1.5rem;
}
.map-container {
max-height: 60vh;
}
}
.world-clock-card {
background: hsl(var(--color-card));
border-radius: var(--radius-lg);
padding: 1rem;
border: 1px solid hsl(var(--color-border));
}
.city-name {
font-weight: 500;
color: hsl(var(--color-foreground));
}
.time-display {
font-size: 2.5rem;
font-weight: 300;
font-variant-numeric: tabular-nums;
color: hsl(var(--color-foreground));
line-height: 1;
}
.timezone-info {
font-size: 0.8125rem;
color: hsl(var(--color-muted-foreground));
}
</style>