mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(manacore/web): theme-aware AppViews with inline creation
Rewrite Todo, Calendar, and Contacts AppView components for the workbench paper-sheet pages: - Theme-aware styling (light paper-sheet + dark mode) - Todo: quick-add input, checkbox toggle, filter tabs - Calendar: week strip with event dots, inline event creation - Contacts: search + inline create form (name, email) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ea28b9065
commit
eabd9200a3
3 changed files with 709 additions and 83 deletions
|
|
@ -1,11 +1,13 @@
|
|||
<!--
|
||||
Calendar — Split-Screen AppView
|
||||
Mini week view with today's events.
|
||||
Calendar — Workbench AppView
|
||||
Mini week view with today's events + quick event creation.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalEvent } from './types';
|
||||
import { eventsStore } from './stores/events.svelte';
|
||||
import { Plus } from '@manacore/shared-icons';
|
||||
|
||||
let events = $state<LocalEvent[]>([]);
|
||||
|
||||
|
|
@ -15,7 +17,7 @@
|
|||
const weekDays = $derived(() => {
|
||||
const days: Date[] = [];
|
||||
const start = new Date(now);
|
||||
start.setDate(start.getDate() - start.getDay() + 1); // Monday
|
||||
start.setDate(start.getDate() - start.getDay() + 1);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(start);
|
||||
d.setDate(d.getDate() + i);
|
||||
|
|
@ -47,32 +49,83 @@
|
|||
}
|
||||
|
||||
const dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
|
||||
// Quick event creation
|
||||
let showNewEvent = $state(false);
|
||||
let newTitle = $state('');
|
||||
let newTime = $state('09:00');
|
||||
|
||||
async function createEvent() {
|
||||
const title = newTitle.trim();
|
||||
if (!title) return;
|
||||
const startTime = `${todayStr}T${newTime}:00`;
|
||||
const [h, m] = newTime.split(':').map(Number);
|
||||
const endH = h + 1;
|
||||
const endTime = `${todayStr}T${String(endH).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`;
|
||||
|
||||
// Get default calendar or use a fallback id
|
||||
const calendars = await db.table('calendars').toArray();
|
||||
const defaultCal = calendars.find((c: Record<string, unknown>) => !c.deletedAt);
|
||||
const calendarId = defaultCal?.id ?? 'default';
|
||||
|
||||
await eventsStore.createEvent({
|
||||
calendarId,
|
||||
title,
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
newTitle = '';
|
||||
showNewEvent = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-4">
|
||||
<div class="app-view">
|
||||
<!-- Mini week strip -->
|
||||
<div class="grid grid-cols-7 gap-1">
|
||||
<div class="week-strip">
|
||||
{#each weekDays() as day, i}
|
||||
{@const isToday = day.toISOString().split('T')[0] === todayStr}
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<span class="text-[10px] text-white/40">{dayNames[i]}</span>
|
||||
<span
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full text-xs
|
||||
{isToday ? 'bg-blue-500 text-white' : 'text-white/60'}"
|
||||
>
|
||||
{@const dayEvents = events.filter((e) =>
|
||||
e.startDate.startsWith(day.toISOString().split('T')[0])
|
||||
)}
|
||||
<div class="day-col">
|
||||
<span class="day-name">{dayNames[i]}</span>
|
||||
<span class="day-num" class:today={isToday}>
|
||||
{day.getDate()}
|
||||
</span>
|
||||
{#if dayEvents.length > 0}
|
||||
<span class="day-dot"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Today's events -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<h3 class="mb-2 text-xs font-medium text-white/50">Heute</h3>
|
||||
<div class="events-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">Heute</h3>
|
||||
<button class="add-btn" onclick={() => (showNewEvent = !showNewEvent)} title="Neuer Termin">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showNewEvent}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createEvent();
|
||||
}}
|
||||
class="new-event-form"
|
||||
>
|
||||
<input bind:value={newTitle} placeholder="Termin-Titel..." class="event-input" autofocus />
|
||||
<input bind:value={newTime} type="time" class="event-time" />
|
||||
<button type="submit" class="event-submit">OK</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#each todayEvents as event (event.id)}
|
||||
<div class="mb-2 rounded-md border border-white/10 bg-white/5 px-3 py-2">
|
||||
<p class="text-sm font-medium text-white/80">{event.title}</p>
|
||||
<p class="text-xs text-white/40">
|
||||
<div class="event-card">
|
||||
<p class="event-title">{event.title}</p>
|
||||
<p class="event-time-label">
|
||||
{#if event.allDay}
|
||||
Ganztägig
|
||||
{:else}
|
||||
|
|
@ -80,13 +133,203 @@
|
|||
{/if}
|
||||
</p>
|
||||
{#if event.location}
|
||||
<p class="text-xs text-white/30">{event.location}</p>
|
||||
<p class="event-location">{event.location}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if todayEvents.length === 0}
|
||||
<p class="py-8 text-center text-sm text-white/30">Keine Termine heute</p>
|
||||
{#if todayEvents.length === 0 && !showNewEvent}
|
||||
<p class="empty">Keine Termine heute</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
.week-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.day-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.day-name {
|
||||
font-size: 0.625rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
:global(.dark) .day-name {
|
||||
color: #6b7280;
|
||||
}
|
||||
.day-num {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
.day-num.today {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
:global(.dark) .day-num {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.day-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 9999px;
|
||||
background: #3b82f6;
|
||||
}
|
||||
.events-section {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.add-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #3b82f6;
|
||||
}
|
||||
:global(.dark) .add-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #60a5fa;
|
||||
}
|
||||
.new-event-form {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
animation: slideDown 0.15s ease-out;
|
||||
}
|
||||
:global(.dark) .new-event-form {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.event-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
min-width: 0;
|
||||
}
|
||||
.event-input::placeholder {
|
||||
color: #c0bfba;
|
||||
}
|
||||
:global(.dark) .event-input {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .event-input::placeholder {
|
||||
color: #4b5563;
|
||||
}
|
||||
.event-time {
|
||||
width: 5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
:global(.dark) .event-time {
|
||||
color: #9ca3af;
|
||||
color-scheme: dark;
|
||||
}
|
||||
.event-submit {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.event-card {
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
:global(.dark) .event-card {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.event-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
:global(.dark) .event-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.event-time-label {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
.event-location {
|
||||
font-size: 0.6875rem;
|
||||
color: #b0afa8;
|
||||
margin: 0;
|
||||
}
|
||||
.empty {
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<!--
|
||||
Contacts — Split-Screen AppView
|
||||
Contact list with search.
|
||||
Contacts — Workbench AppView
|
||||
Contact list with search + quick create.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db } from '$lib/data/database';
|
||||
import type { LocalContact } from './types';
|
||||
import { contactsStore } from './stores/contacts.svelte';
|
||||
import { Plus, Star } from '@manacore/shared-icons';
|
||||
|
||||
let contacts = $state<LocalContact[]>([]);
|
||||
let search = $state('');
|
||||
|
|
@ -44,39 +46,275 @@
|
|||
const l = c.lastName?.[0] ?? '';
|
||||
return (f + l).toUpperCase() || '?';
|
||||
}
|
||||
|
||||
// Quick create
|
||||
let showCreate = $state(false);
|
||||
let newFirstName = $state('');
|
||||
let newLastName = $state('');
|
||||
let newEmail = $state('');
|
||||
|
||||
async function createContact() {
|
||||
const firstName = newFirstName.trim();
|
||||
const lastName = newLastName.trim();
|
||||
if (!firstName && !lastName) return;
|
||||
await contactsStore.createContact({
|
||||
firstName: firstName || undefined,
|
||||
lastName: lastName || undefined,
|
||||
email: newEmail.trim() || undefined,
|
||||
});
|
||||
newFirstName = '';
|
||||
newLastName = '';
|
||||
newEmail = '';
|
||||
showCreate = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-3 p-4">
|
||||
<input
|
||||
bind:value={search}
|
||||
placeholder="Kontakt suchen..."
|
||||
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none"
|
||||
/>
|
||||
<div class="app-view">
|
||||
<div class="search-row">
|
||||
<input bind:value={search} placeholder="Kontakt suchen..." class="search-input" />
|
||||
<button class="add-btn" onclick={() => (showCreate = !showCreate)} title="Neuer Kontakt">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-white/40">{filtered().length} Kontakte</p>
|
||||
{#if showCreate}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createContact();
|
||||
}}
|
||||
class="create-form"
|
||||
>
|
||||
<div class="form-row">
|
||||
<input bind:value={newFirstName} placeholder="Vorname" class="form-input" autofocus />
|
||||
<input bind:value={newLastName} placeholder="Nachname" class="form-input" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input
|
||||
bind:value={newEmail}
|
||||
placeholder="E-Mail (optional)"
|
||||
class="form-input full"
|
||||
type="email"
|
||||
/>
|
||||
<button type="submit" class="form-submit">Anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 overflow-auto">
|
||||
<p class="count">{filtered().length} Kontakte</p>
|
||||
|
||||
<div class="contact-list">
|
||||
{#each filtered() as contact (contact.id)}
|
||||
<div class="flex items-center gap-3 rounded-md px-2 py-2 transition-colors hover:bg-white/5">
|
||||
<div
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-white/10 text-xs font-medium text-white/60"
|
||||
>
|
||||
{initials(contact)}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm text-white/80">{displayName(contact)}</p>
|
||||
<div class="contact-item">
|
||||
<div class="avatar">{initials(contact)}</div>
|
||||
<div class="contact-info">
|
||||
<p class="contact-name">{displayName(contact)}</p>
|
||||
{#if contact.company}
|
||||
<p class="truncate text-xs text-white/40">{contact.company}</p>
|
||||
<p class="contact-company">{contact.company}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if contact.isFavorite}
|
||||
<span class="text-yellow-400 text-xs">★</span>
|
||||
<span class="fav"><Star size={12} weight="fill" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if filtered().length === 0}
|
||||
<p class="py-8 text-center text-sm text-white/30">Keine Kontakte gefunden</p>
|
||||
<p class="empty">Keine Kontakte gefunden</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: transparent;
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
outline: none;
|
||||
}
|
||||
.search-input::placeholder {
|
||||
color: #c0bfba;
|
||||
}
|
||||
.search-input:focus {
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
:global(.dark) .search-input {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .search-input::placeholder {
|
||||
color: #4b5563;
|
||||
}
|
||||
:global(.dark) .search-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.add-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #22c55e;
|
||||
}
|
||||
:global(.dark) .add-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #4ade80;
|
||||
}
|
||||
.create-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
animation: slideDown 0.15s ease-out;
|
||||
}
|
||||
:global(.dark) .create-form {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 0.3125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: transparent;
|
||||
font-size: 0.75rem;
|
||||
color: #374151;
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.form-input::placeholder {
|
||||
color: #c0bfba;
|
||||
}
|
||||
:global(.dark) .form-input {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .form-input::placeholder {
|
||||
color: #4b5563;
|
||||
}
|
||||
.form-input.full {
|
||||
flex: 2;
|
||||
}
|
||||
.form-submit {
|
||||
padding: 0.3125rem 0.625rem;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.count {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.contact-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.375rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.contact-item:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
:global(.dark) .contact-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 9999px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
:global(.dark) .avatar {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #9ca3af;
|
||||
}
|
||||
.contact-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.contact-name {
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:global(.dark) .contact-name {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
.contact-company {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.fav {
|
||||
color: #f59e0b;
|
||||
display: flex;
|
||||
}
|
||||
.empty {
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<!--
|
||||
Todo — Split-Screen AppView
|
||||
Todo — Workbench AppView
|
||||
Compact task list with quick add, filter by inbox/today/overdue.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { liveQuery } from 'dexie';
|
||||
import {
|
||||
useAllTasks,
|
||||
filterIncomplete,
|
||||
|
|
@ -13,6 +12,7 @@
|
|||
getTaskStats,
|
||||
} from './queries';
|
||||
import { tasksStore } from './stores/tasks.svelte';
|
||||
import { Circle, Check } from '@manacore/shared-icons';
|
||||
|
||||
type ViewFilter = 'inbox' | 'today' | 'overdue';
|
||||
|
||||
|
|
@ -44,7 +44,9 @@
|
|||
async function addTask() {
|
||||
const title = newTitle.trim();
|
||||
if (!title) return;
|
||||
await tasksStore.createTask({ title });
|
||||
const data: Record<string, unknown> = { title };
|
||||
if (filter === 'today') data.dueDate = new Date().toISOString();
|
||||
await tasksStore.createTask(data as { title: string; dueDate?: string });
|
||||
newTitle = '';
|
||||
}
|
||||
|
||||
|
|
@ -53,75 +55,218 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-3 p-4">
|
||||
<!-- Stats -->
|
||||
<div class="flex gap-3 text-xs text-white/50">
|
||||
<div class="app-view">
|
||||
<div class="stats">
|
||||
<span>{stats.total} gesamt</span>
|
||||
<span>{stats.today} heute</span>
|
||||
<span class:text-red-400={stats.overdue > 0}>{stats.overdue} überfällig</span>
|
||||
<span class:overdue={stats.overdue > 0}>{stats.overdue} überfällig</span>
|
||||
</div>
|
||||
|
||||
<!-- Filter tabs -->
|
||||
<div class="flex gap-1">
|
||||
<div class="filter-tabs">
|
||||
{#each ['inbox', 'today', 'overdue'] as f}
|
||||
<button
|
||||
onclick={() => (filter = f as ViewFilter)}
|
||||
class="rounded-md px-2.5 py-1 text-xs transition-colors
|
||||
{filter === f ? 'bg-white/15 text-white' : 'text-white/50 hover:text-white/70'}"
|
||||
class="filter-tab"
|
||||
class:active={filter === f}
|
||||
>
|
||||
{f === 'inbox' ? 'Inbox' : f === 'today' ? 'Heute' : 'Überfällig'}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Quick add -->
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
addTask();
|
||||
}}
|
||||
class="flex gap-2"
|
||||
class="quick-add"
|
||||
>
|
||||
<input
|
||||
bind:value={newTitle}
|
||||
placeholder="Neue Aufgabe..."
|
||||
class="flex-1 rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-white/10 px-3 py-1.5 text-sm text-white/70 transition-colors hover:bg-white/15"
|
||||
>+</button
|
||||
>
|
||||
<span class="add-icon"><Circle size={18} /></span>
|
||||
<input bind:value={newTitle} placeholder="Neue Aufgabe..." class="add-input" />
|
||||
</form>
|
||||
|
||||
<!-- Task list -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="task-list">
|
||||
{#each filtered() as task (task.id)}
|
||||
<button
|
||||
onclick={() => toggle(task.id)}
|
||||
class="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-white/5"
|
||||
>
|
||||
<div
|
||||
class="mt-0.5 h-4 w-4 shrink-0 rounded border transition-colors
|
||||
{task.isCompleted ? 'border-green-500 bg-green-500/20' : 'border-white/20'}"
|
||||
></div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p
|
||||
class="truncate text-sm {task.isCompleted
|
||||
? 'text-white/30 line-through'
|
||||
: 'text-white/80'}"
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
<button onclick={() => toggle(task.id)} class="task-item">
|
||||
<div class="checkbox" class:checked={task.isCompleted}>
|
||||
{#if task.isCompleted}<Check size={12} />{/if}
|
||||
</div>
|
||||
<div class="task-content">
|
||||
<p class="task-title" class:completed={task.isCompleted}>{task.title}</p>
|
||||
{#if task.dueDate}
|
||||
<p class="text-xs text-white/30">{new Date(task.dueDate).toLocaleDateString('de')}</p>
|
||||
<p class="task-due">{new Date(task.dueDate).toLocaleDateString('de')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if filtered().length === 0}
|
||||
<p class="py-8 text-center text-sm text-white/30">Keine Aufgaben</p>
|
||||
<p class="empty">Keine Aufgaben</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.overdue {
|
||||
color: #ef4444;
|
||||
}
|
||||
:global(.dark) .stats {
|
||||
color: #6b7280;
|
||||
}
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.filter-tab {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.filter-tab:hover {
|
||||
color: #374151;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.filter-tab.active {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #374151;
|
||||
}
|
||||
:global(.dark) .filter-tab:hover {
|
||||
color: #e5e7eb;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
:global(.dark) .filter-tab.active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.quick-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: transparent;
|
||||
}
|
||||
:global(.dark) .quick-add {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.add-icon {
|
||||
color: #d1d5db;
|
||||
display: flex;
|
||||
}
|
||||
:global(.dark) .add-icon {
|
||||
color: #4b5563;
|
||||
}
|
||||
.add-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
}
|
||||
.add-input::placeholder {
|
||||
color: #c0bfba;
|
||||
}
|
||||
:global(.dark) .add-input {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
:global(.dark) .add-input::placeholder {
|
||||
color: #4b5563;
|
||||
}
|
||||
.task-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.task-item:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
:global(.dark) .task-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 0.125rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 0.25rem;
|
||||
border: 1.5px solid #d1d5db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.checkbox.checked {
|
||||
border-color: #22c55e;
|
||||
background: #22c55e;
|
||||
color: white;
|
||||
}
|
||||
:global(.dark) .checkbox {
|
||||
border-color: #4b5563;
|
||||
}
|
||||
.task-content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.task-title {
|
||||
font-size: 0.8125rem;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.task-title.completed {
|
||||
color: #9ca3af;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
:global(.dark) .task-title {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
:global(.dark) .task-title.completed {
|
||||
color: #6b7280;
|
||||
}
|
||||
.task-due {
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
.empty {
|
||||
padding: 2rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.8125rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue