mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat(ux): notepad design, keyboard navigation, contenteditable across todo/calendar/contacts
Todo: Notepad redesign (A4 width, lined paper, red margin), contenteditable titles, Arrow/Tab navigation between tasks, circular navigation with QuickInputBar, debug borders (Ctrl+Shift+D), removed hover effects, collapsed completed section. Calendar Agenda: contenteditable event titles, Arrow/Tab navigation, circular navigation with InputBar, removed hover effects, separate detail button. Contacts Alphabet: contenteditable contact names, Arrow/Tab navigation, circular navigation with InputBar, removed card hover, avatar as detail button. Guidelines: new sections for list navigation, contenteditable patterns, debug borders. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b9232438cf
commit
4a5fe3bee8
14 changed files with 1150 additions and 365 deletions
|
|
@ -346,6 +346,132 @@ Text: "Diese Aufgabe wird unwiderruflich gelöscht."
|
||||||
| Abbrechen/Schließen | `Escape` |
|
| Abbrechen/Schließen | `Escape` |
|
||||||
| Navigation | `Tab` / `Shift+Tab` |
|
| Navigation | `Tab` / `Shift+Tab` |
|
||||||
|
|
||||||
|
### Listen-Navigation mit Tastatur
|
||||||
|
|
||||||
|
In Listen (Tasks, Kontakte, Favoriten etc.) soll der Nutzer sich nahtlos per Tastatur bewegen können, ohne dass Fokus auf nicht-editierbare Elemente (Checkboxes, Drag-Handles) springt.
|
||||||
|
|
||||||
|
**Prinzip: Zirkuläre Navigation zwischen Input und Liste**
|
||||||
|
|
||||||
|
```
|
||||||
|
InputBar → ArrowUp/Tab → Erster Listeneintrag
|
||||||
|
↕ ArrowUp/Down/Tab/Shift+Tab
|
||||||
|
Letzter Eintrag → ArrowDown/Tab → InputBar
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementierung mit `contenteditable`:**
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- Immer editierbar, kein Mode-Switch -->
|
||||||
|
<span
|
||||||
|
contenteditable="true"
|
||||||
|
role="textbox"
|
||||||
|
spellcheck="true"
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
onblur={handleBlur}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keydown-Handler für Listen-Navigation:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
currentRef?.blur(); // Speichert via onblur
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
currentRef.textContent = originalValue; // Verwerfen
|
||||||
|
currentRef?.blur();
|
||||||
|
} else if (e.key === 'Tab' || e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
const direction = e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey) ? -1 : 1;
|
||||||
|
e.preventDefault();
|
||||||
|
// Alle editierbaren Elemente auf der Seite sammeln
|
||||||
|
const allEditable = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.item-title[contenteditable]')
|
||||||
|
);
|
||||||
|
const currentIndex = allEditable.indexOf(currentRef!);
|
||||||
|
const next = allEditable[currentIndex + direction];
|
||||||
|
currentRef?.blur();
|
||||||
|
if (next) {
|
||||||
|
next.focus();
|
||||||
|
} else {
|
||||||
|
// Am Rand der Liste: Fokus zur InputBar
|
||||||
|
document.querySelector<HTMLInputElement>('.quick-input-bar input')?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Von der InputBar in die Liste (Page-Level Handler):**
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<svelte:window onkeydown={(e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target.closest('.quick-input-bar') && (e.key === 'ArrowUp' || e.key === 'Tab')) {
|
||||||
|
const first = document.querySelector<HTMLElement>('.item-title[contenteditable]');
|
||||||
|
if (first) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regeln:**
|
||||||
|
|
||||||
|
- `contenteditable` statt Input-Toggle: Cursor landet direkt an Klick-Position
|
||||||
|
- Kein visueller Unterschied zwischen Lese- und Edit-Modus (kein Border, kein Hintergrund)
|
||||||
|
- Speichern via `blur`-Event (Auto-Save), nicht via explizitem Save-Button
|
||||||
|
- ArrowDown/Up navigiert vertikal, Tab/Shift+Tab ebenfalls (gleiche Richtung)
|
||||||
|
- Am Listenende/-anfang springt Fokus zurück zur InputBar (zirkulär)
|
||||||
|
- Hover-Effekte auf Listeneinträgen vermeiden (ruhige UI)
|
||||||
|
|
||||||
|
### Inline-Editing mit contenteditable
|
||||||
|
|
||||||
|
**Bevorzuge `contenteditable` statt Input-Toggle** für Texte, die direkt in der Ansicht editiert werden.
|
||||||
|
|
||||||
|
| Aspekt | Input-Toggle | contenteditable |
|
||||||
|
| -------------------- | ------------------------------ | ---------------------------- |
|
||||||
|
| Klicks zum Editieren | 2 (aktivieren + positionieren) | 1 (Cursor an Klick-Position) |
|
||||||
|
| DOM-Wechsel | `<span>` ↔ `<input>` | Keiner |
|
||||||
|
| Mehrzeilig | Braucht `<textarea>` | Nativ |
|
||||||
|
| Styling | Muss Input an Span anpassen | Gleich |
|
||||||
|
|
||||||
|
**Wann Input statt contenteditable:**
|
||||||
|
|
||||||
|
- Formular-Felder mit Validierung (Datum, Zahl, E-Mail)
|
||||||
|
- Wenn `type="date"`, `type="number"` etc. benötigt wird
|
||||||
|
- In Expanded-Forms/Modals (nicht inline)
|
||||||
|
|
||||||
|
### Debug-Borders (Entwickler-Tool)
|
||||||
|
|
||||||
|
`Ctrl+Shift+D` aktiviert farbkodierte Outlines für alle UI-Elemente. Nutzt `outline` statt `border` um Layout nicht zu beeinflussen. State wird in localStorage persistiert.
|
||||||
|
|
||||||
|
```css
|
||||||
|
.debug-mode * {
|
||||||
|
outline: 1px solid rgba(255, 0, 0, 0.3) !important;
|
||||||
|
}
|
||||||
|
.debug-mode div {
|
||||||
|
outline-color: rgba(255, 0, 0, 0.4) !important;
|
||||||
|
}
|
||||||
|
.debug-mode section {
|
||||||
|
outline-color: rgba(0, 100, 255, 0.5) !important;
|
||||||
|
}
|
||||||
|
.debug-mode nav {
|
||||||
|
outline-color: rgba(0, 180, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
.debug-mode button {
|
||||||
|
outline-color: rgba(255, 220, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
.debug-mode input,
|
||||||
|
.debug-mode textarea {
|
||||||
|
outline-color: rgba(139, 92, 246, 0.6) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Aktuell in Todo implementiert, geplant als `@manacore/shared-debug` Package.
|
||||||
|
|
||||||
## Z-Index-Hierarchie
|
## Z-Index-Hierarchie
|
||||||
|
|
||||||
Konsistente Schichtung für Overlays:
|
Konsistente Schichtung für Overlays:
|
||||||
|
|
@ -390,6 +516,10 @@ font-bold: 700 /* Wichtige Titel */
|
||||||
- [ ] Inline-Interaktion statt Modal möglich?
|
- [ ] Inline-Interaktion statt Modal möglich?
|
||||||
- [ ] Loading/Error/Empty States vorhanden?
|
- [ ] Loading/Error/Empty States vorhanden?
|
||||||
- [ ] Tastatur-Navigation funktioniert?
|
- [ ] Tastatur-Navigation funktioniert?
|
||||||
|
- [ ] Listen: Arrow/Tab-Navigation zwischen Einträgen?
|
||||||
|
- [ ] Listen: Zirkuläre Navigation zu InputBar?
|
||||||
|
- [ ] Inline-Editing: contenteditable statt Input-Toggle?
|
||||||
|
- [ ] Keine Hover-Effekte auf Listeneinträgen?
|
||||||
- [ ] Animations sparsam und sinnvoll?
|
- [ ] Animations sparsam und sinnvoll?
|
||||||
- [ ] CSS-Variablen statt hardcoded Farben?
|
- [ ] CSS-Variablen statt hardcoded Farben?
|
||||||
- [ ] Konsistente Spacing-Werte?
|
- [ ] Konsistente Spacing-Werte?
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline title editing
|
||||||
|
function handleTitleBlur(event: CalendarEvent, el: HTMLSpanElement) {
|
||||||
|
const trimmed = (el.textContent || '').trim();
|
||||||
|
if (trimmed && trimmed !== event.title) {
|
||||||
|
eventsStore.updateEvent(event.id, { title: trimmed });
|
||||||
|
} else {
|
||||||
|
el.textContent = event.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTitleKeydown(e: KeyboardEvent, event: CalendarEvent) {
|
||||||
|
const target = e.target as HTMLSpanElement;
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
target.blur();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
target.textContent = event.title;
|
||||||
|
target.blur();
|
||||||
|
} else if (e.key === 'Tab' || e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
const direction = e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey) ? -1 : 1;
|
||||||
|
e.preventDefault();
|
||||||
|
const allTitles = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.agenda-event-title[contenteditable]')
|
||||||
|
);
|
||||||
|
const currentIndex = allTitles.indexOf(target);
|
||||||
|
const next = allTitles[currentIndex + direction];
|
||||||
|
target.blur();
|
||||||
|
if (next) {
|
||||||
|
next.focus();
|
||||||
|
} else {
|
||||||
|
document.querySelector<HTMLInputElement>('.quick-input-bar input')?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Context menu state
|
// Context menu state
|
||||||
let contextMenuVisible = $state(false);
|
let contextMenuVisible = $state(false);
|
||||||
let contextMenuX = $state(0);
|
let contextMenuX = $state(0);
|
||||||
|
|
@ -180,9 +215,9 @@
|
||||||
|
|
||||||
<div class="events-for-date">
|
<div class="events-for-date">
|
||||||
{#each group.events as event}
|
{#each group.events as event}
|
||||||
<button
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
class="event-item"
|
class="event-item"
|
||||||
onclick={() => handleEventClick(event)}
|
|
||||||
oncontextmenu={(e) => {
|
oncontextmenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -207,7 +242,18 @@
|
||||||
)}
|
)}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-title">{event.title}</div>
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<span
|
||||||
|
class="event-title agenda-event-title"
|
||||||
|
contenteditable="true"
|
||||||
|
role="textbox"
|
||||||
|
spellcheck="true"
|
||||||
|
onkeydown={(e) => handleTitleKeydown(e, event)}
|
||||||
|
onblur={(e) => handleTitleBlur(event, e.target as HTMLSpanElement)}
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</span>
|
||||||
{#if event.location}
|
{#if event.location}
|
||||||
<div class="event-location">
|
<div class="event-location">
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -233,15 +279,22 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<svg class="chevron-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<button
|
||||||
<path
|
class="expand-btn"
|
||||||
stroke-linecap="round"
|
onclick={() => handleEventClick(event)}
|
||||||
stroke-linejoin="round"
|
title="Details öffnen"
|
||||||
stroke-width="2"
|
aria-label="Details öffnen"
|
||||||
d="M9 5l7 7-7 7"
|
>
|
||||||
/>
|
<svg class="chevron-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
</svg>
|
<path
|
||||||
</button>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -329,25 +382,14 @@
|
||||||
|
|
||||||
.event-item {
|
.event-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid hsl(var(--color-border));
|
border: 1px solid hsl(var(--color-border));
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: hsl(var(--color-surface));
|
background: hsl(var(--color-surface));
|
||||||
transition:
|
|
||||||
transform 150ms ease,
|
|
||||||
box-shadow 150ms ease,
|
|
||||||
border-color 150ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-item:hover {
|
|
||||||
transform: translateX(4px);
|
|
||||||
border-color: hsl(var(--color-border-hover, var(--color-border)));
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-bar {
|
.color-bar {
|
||||||
|
|
@ -376,9 +418,13 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
color: hsl(var(--color-foreground));
|
color: hsl(var(--color-foreground));
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
overflow: hidden;
|
word-break: break-word;
|
||||||
text-overflow: ellipsis;
|
cursor: text;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.0625rem 0.125rem;
|
||||||
|
margin: -0.0625rem -0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-location {
|
.event-location {
|
||||||
|
|
@ -396,15 +442,30 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expand-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.375rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: hsl(var(--color-surface-hover, var(--color-surface)));
|
||||||
|
}
|
||||||
|
|
||||||
.chevron-icon {
|
.chevron-icon {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: hsl(var(--color-muted-foreground));
|
||||||
opacity: 0.5;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-item:hover .chevron-icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { viewStore } from '$lib/stores/view.svelte';
|
||||||
import { eventsStore } from '$lib/stores/events.svelte';
|
import { eventsStore } from '$lib/stores/events.svelte';
|
||||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||||
import { todosStore } from '$lib/stores/todos.svelte';
|
import { todosStore } from '$lib/stores/todos.svelte';
|
||||||
|
|
@ -142,6 +143,23 @@
|
||||||
<title>{$_('app.name')}</title>
|
<title>{$_('app.name')}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (viewStore.viewType !== 'agenda') return;
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const isInQuickInput = target.closest('.quick-input-bar');
|
||||||
|
if (isInQuickInput && (e.key === 'ArrowUp' || (e.key === 'Tab' && !e.shiftKey))) {
|
||||||
|
const firstTitle = document.querySelector<HTMLElement>(
|
||||||
|
'.agenda-event-title[contenteditable]'
|
||||||
|
);
|
||||||
|
if (firstTitle) {
|
||||||
|
e.preventDefault();
|
||||||
|
firstTitle.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="service-banners">
|
<div class="service-banners">
|
||||||
<ServiceStatusBanner
|
<ServiceStatusBanner
|
||||||
serviceName="Todo-Service"
|
serviceName="Todo-Service"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
|
import { newContactModalStore } from '$lib/stores/new-contact-modal.svelte';
|
||||||
import { contactsFilterStore } from '$lib/stores/filter.svelte';
|
import { contactsFilterStore } from '$lib/stores/filter.svelte';
|
||||||
import { contactsSettings } from '$lib/stores/settings.svelte';
|
import { contactsSettings } from '$lib/stores/settings.svelte';
|
||||||
|
import { contactsStore } from '$lib/stores/contacts.svelte';
|
||||||
import AlphabetNavContextMenu from '$lib/components/AlphabetNavContextMenu.svelte';
|
import AlphabetNavContextMenu from '$lib/components/AlphabetNavContextMenu.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -100,6 +101,50 @@
|
||||||
// Available letters (letters that have contacts)
|
// Available letters (letters that have contacts)
|
||||||
let availableLetters = $derived(Object.keys(groupedContacts).sort());
|
let availableLetters = $derived(Object.keys(groupedContacts).sort());
|
||||||
|
|
||||||
|
// Inline name editing
|
||||||
|
function handleNameBlur(contact: Contact, el: HTMLSpanElement) {
|
||||||
|
const trimmed = (el.textContent || '').trim();
|
||||||
|
const currentName = getDisplayName(contact);
|
||||||
|
if (trimmed && trimmed !== currentName) {
|
||||||
|
// Parse display name back into first/last name
|
||||||
|
const parts = trimmed.split(/\s+/);
|
||||||
|
const firstName = parts[0] || '';
|
||||||
|
const lastName = parts.slice(1).join(' ') || '';
|
||||||
|
contactsStore.updateContact(contact.id, {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
displayName: trimmed,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
el.textContent = currentName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNameKeydown(e: KeyboardEvent, contact: Contact) {
|
||||||
|
const target = e.target as HTMLSpanElement;
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
target.blur();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
target.textContent = getDisplayName(contact);
|
||||||
|
target.blur();
|
||||||
|
} else if (e.key === 'Tab' || e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
const direction = e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey) ? -1 : 1;
|
||||||
|
e.preventDefault();
|
||||||
|
const allNames = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.contact-name-editable[contenteditable]')
|
||||||
|
);
|
||||||
|
const currentIndex = allNames.indexOf(target);
|
||||||
|
const next = allNames[currentIndex + direction];
|
||||||
|
target.blur();
|
||||||
|
if (next) {
|
||||||
|
next.focus();
|
||||||
|
} else {
|
||||||
|
document.querySelector<HTMLInputElement>('.quick-input-bar input')?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scrollToLetter(letter: string) {
|
function scrollToLetter(letter: string) {
|
||||||
const element = document.getElementById(`section-${letter}`);
|
const element = document.getElementById(`section-${letter}`);
|
||||||
if (element) {
|
if (element) {
|
||||||
|
|
@ -162,10 +207,6 @@
|
||||||
<div class="section-contacts">
|
<div class="section-contacts">
|
||||||
{#each groupedContacts[letter] as contact (contact.id)}
|
{#each groupedContacts[letter] as contact (contact.id)}
|
||||||
<div
|
<div
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
onclick={() => onContactClick(contact.id)}
|
|
||||||
onkeydown={(e) => e.key === 'Enter' && onContactClick(contact.id)}
|
|
||||||
class="alphabet-contact-card {selectionMode && selectedIds.has(contact.id)
|
class="alphabet-contact-card {selectionMode && selectedIds.has(contact.id)
|
||||||
? 'selected'
|
? 'selected'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|
@ -188,8 +229,12 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Avatar — click opens detail -->
|
||||||
<div class="avatar-sm">
|
<button
|
||||||
|
class="avatar-sm avatar-btn"
|
||||||
|
onclick={() => onContactClick(contact.id)}
|
||||||
|
title="Details öffnen"
|
||||||
|
>
|
||||||
{#if contact.photoUrl}
|
{#if contact.photoUrl}
|
||||||
<img
|
<img
|
||||||
src={contact.photoUrl}
|
src={contact.photoUrl}
|
||||||
|
|
@ -199,12 +244,23 @@
|
||||||
{:else}
|
{:else}
|
||||||
{getInitials(contact)}
|
{getInitials(contact)}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
<!-- Contact Info -->
|
<!-- Contact Info -->
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="contact-main-row">
|
<div class="contact-main-row">
|
||||||
<span class="contact-name">{getDisplayName(contact)}</span>
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<span
|
||||||
|
class="contact-name contact-name-editable"
|
||||||
|
contenteditable="true"
|
||||||
|
role="textbox"
|
||||||
|
spellcheck="true"
|
||||||
|
onkeydown={(e) => handleNameKeydown(e, contact)}
|
||||||
|
onblur={(e) => handleNameBlur(contact, e.target as HTMLSpanElement)}
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{getDisplayName(contact)}
|
||||||
|
</span>
|
||||||
{#if contact.isFavorite}
|
{#if contact.isFavorite}
|
||||||
<svg class="favorite-badge" fill="currentColor" viewBox="0 0 24 24">
|
<svg class="favorite-badge" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
|
@ -238,7 +294,6 @@
|
||||||
{#if contact.phone || contact.mobile}
|
{#if contact.phone || contact.mobile}
|
||||||
<a
|
<a
|
||||||
href="tel:{contact.mobile || contact.phone}"
|
href="tel:{contact.mobile || contact.phone}"
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
class="action-chip"
|
class="action-chip"
|
||||||
title={contact.mobile || contact.phone}
|
title={contact.mobile || contact.phone}
|
||||||
>
|
>
|
||||||
|
|
@ -253,12 +308,7 @@
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if contact.email}
|
{#if contact.email}
|
||||||
<a
|
<a href="mailto:{contact.email}" class="action-chip" title={contact.email}>
|
||||||
href="mailto:{contact.email}"
|
|
||||||
onclick={(e) => e.stopPropagation()}
|
|
||||||
class="action-chip"
|
|
||||||
title={contact.email}
|
|
||||||
>
|
|
||||||
<svg class="action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
|
|
@ -413,16 +463,9 @@
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
cursor: pointer;
|
|
||||||
transition: all var(--transition-fast);
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alphabet-contact-card:hover {
|
|
||||||
border-color: hsl(var(--primary));
|
|
||||||
background-color: hsl(var(--accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-sm {
|
.avatar-sm {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|
@ -460,6 +503,24 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-name-editable {
|
||||||
|
cursor: text;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.0625rem 0.125rem;
|
||||||
|
margin: -0.0625rem -0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-btn {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.favorite-badge {
|
.favorite-badge {
|
||||||
width: 0.8125rem;
|
width: 0.8125rem;
|
||||||
height: 0.8125rem;
|
height: 0.8125rem;
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,20 @@
|
||||||
<title>{$_('contacts.title')} - Contacts</title>
|
<title>{$_('contacts.title')} - Contacts</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
onkeydown={(e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const isInQuickInput = target.closest('.quick-input-bar');
|
||||||
|
if (isInQuickInput && (e.key === 'ArrowUp' || (e.key === 'Tab' && !e.shiftKey))) {
|
||||||
|
const firstName = document.querySelector<HTMLElement>(
|
||||||
|
'.contact-name-editable[contenteditable]'
|
||||||
|
);
|
||||||
|
if (firstName) {
|
||||||
|
e.preventDefault();
|
||||||
|
firstName.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<ContactList />
|
<ContactList />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
---
|
||||||
|
title: 'Todo: Notepad-Design & Keyboard-Navigation'
|
||||||
|
description: 'Komplettes Redesign der Todo-Liste als Notizblock mit durchgängiger Tastaturnavigation zwischen Tasks und QuickInputBar.'
|
||||||
|
date: 2026-03-30
|
||||||
|
author: 'Till Schneider'
|
||||||
|
category: 'feature'
|
||||||
|
tags: ['todo', 'ux', 'keyboard-navigation', 'contenteditable', 'design', 'accessibility', 'notepad']
|
||||||
|
featured: false
|
||||||
|
commits: 42
|
||||||
|
readTime: 5
|
||||||
|
stats:
|
||||||
|
filesChanged: 834
|
||||||
|
linesAdded: 25713
|
||||||
|
linesRemoved: 10299
|
||||||
|
contributors:
|
||||||
|
- name: 'Till Schneider'
|
||||||
|
handle: 'Till-JS'
|
||||||
|
commits: 42
|
||||||
|
workingHours:
|
||||||
|
start: '2026-03-30T09:00'
|
||||||
|
end: '2026-03-30T18:00'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Notizblock-Design mit liniertem Papier-Hintergrund und roter Margin-Linie
|
||||||
|
- Durchgängige Tastaturnavigation: Pfeiltasten, Tab, Shift+Tab zwischen allen Tasks
|
||||||
|
- contenteditable statt Input-Toggle: Cursor landet direkt an der Klick-Position
|
||||||
|
- Zirkuläre Navigation: QuickInputBar -> Tasks -> QuickInputBar
|
||||||
|
- Debug-Borders Modus (Ctrl+Shift+D) für UI-Debugging
|
||||||
|
|
||||||
|
## Notepad-Redesign
|
||||||
|
|
||||||
|
Die Todo-Homepage wurde von einer schmalen Card-Ansicht (560px) zu einem A4-breiten Notizblock (840px) umgebaut.
|
||||||
|
|
||||||
|
### Vorher
|
||||||
|
|
||||||
|
- Kleine weiße Card mit Box-Shadow auf grauem Hintergrund
|
||||||
|
- CollapsibleSection-Komponenten mit Toggle-Logik
|
||||||
|
- Task-Items mit Hover-Effekten (translateY, Hintergrundwechsel)
|
||||||
|
- Separate Input-Elemente für Titel-Bearbeitung (zwei Klicks nötig)
|
||||||
|
|
||||||
|
### Nachher
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ ┃ Überfällig (2) │ ← Rote Margin-Linie
|
||||||
|
│ ┃ ○ Steuererklärung abgeben Gestern │
|
||||||
|
│ ┃ ○ Zahnarzt anrufen 25. Mär │
|
||||||
|
│ ┃─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ← Gestrichelte Trenner
|
||||||
|
│ ┃ Heute (3) │
|
||||||
|
│ ┃ ○ Meeting vorbereiten Heute │
|
||||||
|
│ ┃ ○ Code Review PR #42 Heute │
|
||||||
|
│ ┃ ○ Einkaufen gehen Heute │
|
||||||
|
│ ┃─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
||||||
|
│ ┃ ▼ Erledigt (5) │ ← Standardmäßig eingeklappt
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
Linierter Hintergrund (Notizbuch-Linien)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Design-Entscheidungen:**
|
||||||
|
|
||||||
|
| Aspekt | Entscheidung | Grund |
|
||||||
|
| ------------- | --------------------------------- | ---------------------------------------------- |
|
||||||
|
| Hintergrund | Liniert (#fffef5) nur auf Sheet | Papier-Metapher, Hauptbereich bleibt neutral |
|
||||||
|
| Breite | 840px (A4-ähnlich) | Mehr Platz für lange Titel, Labels, Kontakte |
|
||||||
|
| Sections | Einfache Header statt Collapsible | Weniger UI-Noise, Sections sind immer relevant |
|
||||||
|
| Erledigt | Eingeklappt mit Chevron-Toggle | Aktive Tasks im Fokus |
|
||||||
|
| Drag-Handle | Links außerhalb des Sheets | Stört nicht, erscheint nur bei Hover |
|
||||||
|
| Hover-Effekte | Komplett entfernt | Ruhigere UI, weniger visuelle Ablenkung |
|
||||||
|
|
||||||
|
## Keyboard-Navigation
|
||||||
|
|
||||||
|
### Navigationsmodell
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ QuickInputBar (unten) │ ← ArrowUp / Tab
|
||||||
|
│ [Neue Aufgabe oder...] │
|
||||||
|
└──────────┬──────────────────┘
|
||||||
|
│ ↑ ArrowUp / Tab
|
||||||
|
▼ ↓ ArrowDown / Tab
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ Task 1: Meeting vorber... │ ← contenteditable, Cursor an Klick-Position
|
||||||
|
│ Task 2: Code Review PR... │ ← ArrowDown / Tab → nächster Task
|
||||||
|
│ Task 3: Einkaufen gehen │ ← ArrowDown → zurück zur QuickInputBar
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tastatur-Befehle im Task-Titel
|
||||||
|
|
||||||
|
| Taste | Aktion |
|
||||||
|
| --------------------------- | ------------------------ |
|
||||||
|
| `ArrowDown` / `Tab` | Nächster Task-Titel |
|
||||||
|
| `ArrowUp` / `Shift+Tab` | Vorheriger Task-Titel |
|
||||||
|
| `Enter` | Speichern und Blur |
|
||||||
|
| `Escape` | Änderungen verwerfen |
|
||||||
|
| Am letzten Task `ArrowDown` | Zurück zur QuickInputBar |
|
||||||
|
| Am ersten Task `ArrowUp` | Zurück zur QuickInputBar |
|
||||||
|
|
||||||
|
### Von QuickInputBar zu Tasks
|
||||||
|
|
||||||
|
| Taste | Aktion |
|
||||||
|
| --------- | ----------------------------- |
|
||||||
|
| `ArrowUp` | Erster Task-Titel fokussieren |
|
||||||
|
| `Tab` | Erster Task-Titel fokussieren |
|
||||||
|
|
||||||
|
### contenteditable statt Input-Toggle
|
||||||
|
|
||||||
|
**Vorher:** Klick auf Titel → `<span>` wird durch `<input>` ersetzt → zweiter Klick positioniert Cursor.
|
||||||
|
|
||||||
|
**Nachher:** Titel ist immer ein `<span contenteditable>` → ein Klick positioniert den Cursor sofort an der Maus-Position. Kein visueller Unterschied zwischen Lese- und Bearbeitungsmodus.
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<span
|
||||||
|
class="task-title"
|
||||||
|
contenteditable="true"
|
||||||
|
onkeydown={handleTitleKeydown}
|
||||||
|
onblur={handleTitleBlur}
|
||||||
|
>
|
||||||
|
{task.title}
|
||||||
|
</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debug-Borders
|
||||||
|
|
||||||
|
Neues Entwickler-Tool: `Ctrl+Shift+D` aktiviert farbkodierte Outlines für alle UI-Elemente.
|
||||||
|
|
||||||
|
| Element | Farbe |
|
||||||
|
| ---------------- | ------- |
|
||||||
|
| `div` | Rot |
|
||||||
|
| `section` | Blau |
|
||||||
|
| `nav` | Grün |
|
||||||
|
| `button` | Gelb |
|
||||||
|
| `input/textarea` | Violett |
|
||||||
|
| `a` | Pink |
|
||||||
|
| `img/svg` | Cyan |
|
||||||
|
|
||||||
|
State wird in localStorage persistiert. Aktuell nur in der Todo-App, geplant als globales Shared-Package.
|
||||||
|
|
||||||
|
## Weitere Verbesserungen
|
||||||
|
|
||||||
|
| Bereich | Vorher | Nachher |
|
||||||
|
| ---------------------- | ------------------------ | --------------------------------------- |
|
||||||
|
| Task-Titel | Einzeilig, abgeschnitten | Mehrzeilig mit word-break |
|
||||||
|
| Checkbox-Alignment | Vertikal zentriert | Am Anfang der ersten Zeile (flex-start) |
|
||||||
|
| Mobile Bottom-Padding | 150px | 100px |
|
||||||
|
| Erledigt-Datum Opacity | 0.5 (zu blass) | 0.7, Hover 1.0 |
|
||||||
|
| Task-Hover | translateY + Hintergrund | Kein Hover-Effekt |
|
||||||
|
| Titel-Input | Border + Hintergrund | Unsichtbar, gleiche Position |
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
| Bereich | Commits | Highlights |
|
||||||
|
| ------------------- | ------- | ----------------------------------------------- |
|
||||||
|
| Notepad-Design | ~15 | Liniertes Papier, A4-Breite, Section-Redesign |
|
||||||
|
| Keyboard-Navigation | ~10 | Arrow/Tab zwischen Tasks, zirkulär mit InputBar |
|
||||||
|
| contenteditable | ~5 | Direkte Cursor-Platzierung, kein Mode-Switch |
|
||||||
|
| Debug-Borders | ~3 | Ctrl+Shift+D, farbkodiert, localStorage |
|
||||||
|
| Cleanup | ~9 | Hover entfernt, Alignment, Opacity |
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
- Debug-Borders als globales Shared-Package für alle Apps
|
||||||
|
- Keyboard-Navigation Pattern in andere Apps übertragen (Zitare, Contacts)
|
||||||
|
- Tastatur-Shortcut-Hilfe (? oder Ctrl+/) mit Overlay
|
||||||
|
|
@ -42,13 +42,7 @@
|
||||||
|
|
||||||
/* Task item transitions */
|
/* Task item transitions */
|
||||||
.task-item {
|
.task-item {
|
||||||
transition:
|
transition: none;
|
||||||
transform 0.15s ease,
|
|
||||||
box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-item:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checkbox animations */
|
/* Checkbox animations */
|
||||||
|
|
@ -135,3 +129,63 @@
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
border-left: 3px solid var(--color-primary);
|
border-left: 3px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Debug Mode - Toggle with Ctrl+Shift+D */
|
||||||
|
.debug-mode * {
|
||||||
|
outline: 1px solid rgba(255, 0, 0, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode div {
|
||||||
|
outline-color: rgba(255, 0, 0, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode section {
|
||||||
|
outline-color: rgba(0, 100, 255, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode nav {
|
||||||
|
outline-color: rgba(0, 180, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode main {
|
||||||
|
outline-color: rgba(255, 140, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode header,
|
||||||
|
.debug-mode footer {
|
||||||
|
outline-color: rgba(128, 0, 128, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode button {
|
||||||
|
outline-color: rgba(255, 220, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode input,
|
||||||
|
.debug-mode textarea,
|
||||||
|
.debug-mode select {
|
||||||
|
outline-color: rgba(139, 92, 246, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode a {
|
||||||
|
outline-color: rgba(236, 72, 153, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode img,
|
||||||
|
.debug-mode svg {
|
||||||
|
outline-color: rgba(0, 200, 200, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode ul,
|
||||||
|
.debug-mode ol,
|
||||||
|
.debug-mode li {
|
||||||
|
outline-color: rgba(234, 179, 8, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-mode p,
|
||||||
|
.debug-mode span,
|
||||||
|
.debug-mode h1,
|
||||||
|
.debug-mode h2,
|
||||||
|
.debug-mode h3,
|
||||||
|
.debug-mode h4 {
|
||||||
|
outline-color: rgba(107, 114, 128, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,30 +180,42 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline title editing
|
// Inline title editing
|
||||||
let isEditingTitle = $state(false);
|
let titleRef = $state<HTMLSpanElement | null>(null);
|
||||||
let editingTitle = $state('');
|
|
||||||
let titleEditRef = $state<HTMLInputElement | null>(null);
|
|
||||||
|
|
||||||
function handleTitleClick(e: MouseEvent) {
|
|
||||||
e.stopPropagation();
|
|
||||||
isEditingTitle = true;
|
|
||||||
editingTitle = task.title;
|
|
||||||
setTimeout(() => titleEditRef?.focus(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTitleKeydown(e: KeyboardEvent) {
|
function handleTitleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
commitTitleEdit();
|
e.preventDefault();
|
||||||
|
titleRef?.blur();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
isEditingTitle = false;
|
if (titleRef) titleRef.textContent = task.title;
|
||||||
|
titleRef?.blur();
|
||||||
|
} else if (e.key === 'Tab' || e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
const direction = e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey) ? -1 : 1;
|
||||||
|
e.preventDefault();
|
||||||
|
const allTitles = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>('.task-title[contenteditable]')
|
||||||
|
);
|
||||||
|
const currentIndex = allTitles.indexOf(titleRef!);
|
||||||
|
const nextTitle = allTitles[currentIndex + direction];
|
||||||
|
titleRef?.blur();
|
||||||
|
if (nextTitle) {
|
||||||
|
nextTitle.focus();
|
||||||
|
} else {
|
||||||
|
// No next/prev todo — focus QuickInputBar
|
||||||
|
const input = document.querySelector<HTMLInputElement>('.quick-input-bar input');
|
||||||
|
input?.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitTitleEdit() {
|
function handleTitleBlur() {
|
||||||
isEditingTitle = false;
|
if (!titleRef) return;
|
||||||
const trimmed = editingTitle.trim();
|
const trimmed = (titleRef.textContent || '').trim();
|
||||||
if (trimmed && trimmed !== task.title) {
|
if (trimmed && trimmed !== task.title) {
|
||||||
onSave?.({ title: trimmed });
|
onSave?.({ title: trimmed });
|
||||||
|
} else {
|
||||||
|
// Revert if empty or unchanged
|
||||||
|
titleRef.textContent = task.title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,22 +421,20 @@
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="task-content">
|
<div class="task-content">
|
||||||
{#if isEditingTitle}
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<input
|
<span
|
||||||
bind:this={titleEditRef}
|
bind:this={titleRef}
|
||||||
class="task-title-input"
|
class="task-title"
|
||||||
type="text"
|
class:line-through={task.isCompleted}
|
||||||
bind:value={editingTitle}
|
contenteditable="true"
|
||||||
onkeydown={handleTitleKeydown}
|
role="textbox"
|
||||||
onblur={commitTitleEdit}
|
spellcheck="true"
|
||||||
/>
|
onkeydown={handleTitleKeydown}
|
||||||
{:else}
|
onblur={handleTitleBlur}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
onclick={(e) => e.stopPropagation()}
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
>
|
||||||
<span class="task-title" class:line-through={task.isCompleted} onclick={handleTitleClick}>
|
{task.title}
|
||||||
{task.title}
|
</span>
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Labels and subtasks below title -->
|
<!-- Labels and subtasks below title -->
|
||||||
{#if subtaskProgress() || (task.labels && task.labels.length > 0)}
|
{#if subtaskProgress() || (task.labels && task.labels.length > 0)}
|
||||||
|
|
@ -721,13 +731,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
padding: 0.5rem 0.75rem;
|
min-height: 2.5rem; /* matches notepad --line-height */
|
||||||
|
padding: 0 1.5rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
-webkit-backdrop-filter: none;
|
-webkit-backdrop-filter: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
border-bottom: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
@ -748,21 +759,14 @@
|
||||||
:global(.dark) .task-item {
|
:global(.dark) .task-item {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-item:hover {
|
.task-item:hover {
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: transparent;
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .task-item:hover {
|
:global(.dark) .task-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-item-wrapper.expanded .task-item:hover {
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-item.completed {
|
.task-item.completed {
|
||||||
|
|
@ -780,23 +784,24 @@
|
||||||
border-bottom-color: rgba(34, 197, 94, 0.3);
|
border-bottom-color: rgba(34, 197, 94, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drag handle */
|
/* Drag handle — sticks out left beyond the content area */
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
opacity: 0.25;
|
opacity: 0;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.5rem 0.375rem;
|
padding: 0.25rem 0.25rem;
|
||||||
margin-left: -0.25rem;
|
margin-left: -2rem;
|
||||||
|
margin-right: -0.5rem;
|
||||||
|
margin-top: 0.125rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
min-height: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-item:hover .drag-handle {
|
.task-item:hover .drag-handle {
|
||||||
opacity: 0.5;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag-handle:hover {
|
.drag-handle:hover {
|
||||||
|
|
@ -828,6 +833,7 @@
|
||||||
|
|
||||||
/* Checkbox with priority color fill */
|
/* Checkbox with priority color fill */
|
||||||
.task-checkbox {
|
.task-checkbox {
|
||||||
|
margin-top: 0.1875rem;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
|
@ -947,40 +953,21 @@
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
overflow: hidden;
|
word-break: break-word;
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
padding: 0.125rem 0.25rem;
|
padding: 0.125rem 0.25rem;
|
||||||
margin: -0.125rem -0.25rem;
|
margin: -0.125rem -0.25rem;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-title:hover {
|
.task-title:hover {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .task-title:hover {
|
:global(.dark) .task-title:hover {
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: transparent;
|
||||||
}
|
|
||||||
|
|
||||||
.task-title-input {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #374151;
|
|
||||||
background: rgba(0, 0, 0, 0.04);
|
|
||||||
border: 1px solid #8b5cf6;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.125rem 0.25rem;
|
|
||||||
margin: -0.125rem -0.25rem;
|
|
||||||
outline: none;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .task-title-input {
|
|
||||||
color: #f3f4f6;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
border-color: #8b5cf6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .task-title {
|
:global(.dark) .task-title {
|
||||||
|
|
@ -1084,6 +1071,7 @@
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .due-date {
|
:global(.dark) .due-date {
|
||||||
|
|
@ -1109,12 +1097,12 @@
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.125rem 0;
|
padding: 0.125rem 0;
|
||||||
opacity: 0.5;
|
opacity: 0.7;
|
||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed-date-toggle:hover {
|
.completed-date-toggle:hover {
|
||||||
opacity: 0.8;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.completed-date-toggle .date-label {
|
.completed-date-toggle .date-label {
|
||||||
|
|
|
||||||
|
|
@ -317,16 +317,12 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.task-list {
|
.task-list {
|
||||||
min-height: 40px;
|
min-height: 2.5rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-list :global(.task-item-wrapper:last-child .task-item) {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-list.empty {
|
.task-list.empty {
|
||||||
border: 2px dashed rgba(0, 0, 0, 0.15);
|
border: 2px dashed rgba(0, 0, 0, 0.15);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -362,7 +358,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dnd-shadow-placeholder {
|
.dnd-shadow-placeholder {
|
||||||
min-height: 3rem;
|
min-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shadow placeholder (where dragged item will land) */
|
/* Shadow placeholder (where dragged item will land) */
|
||||||
|
|
|
||||||
21
apps/todo/apps/web/src/lib/stores/debug.svelte.ts
Normal file
21
apps/todo/apps/web/src/lib/stores/debug.svelte.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
const DEBUG_KEY = 'manacore:debug-mode';
|
||||||
|
|
||||||
|
function createDebugStore() {
|
||||||
|
let enabled = $state(
|
||||||
|
typeof localStorage !== 'undefined' ? localStorage.getItem(DEBUG_KEY) === 'true' : false
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get enabled() {
|
||||||
|
return enabled;
|
||||||
|
},
|
||||||
|
toggle() {
|
||||||
|
enabled = !enabled;
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem(DEBUG_KEY, String(enabled));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const debugStore = createDebugStore();
|
||||||
|
|
@ -9,6 +9,17 @@ import type { TaskPriority } from '@todo/shared';
|
||||||
// Settings types
|
// Settings types
|
||||||
export type TodoView = 'inbox' | 'today' | 'upcoming' | 'kanban' | 'completed';
|
export type TodoView = 'inbox' | 'today' | 'upcoming' | 'kanban' | 'completed';
|
||||||
export type KanbanCardSize = 'compact' | 'normal' | 'large';
|
export type KanbanCardSize = 'compact' | 'normal' | 'large';
|
||||||
|
export type PageMode = 'date' | 'priority' | 'custom';
|
||||||
|
|
||||||
|
export interface PageConfig {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
filter: {
|
||||||
|
priorities?: ('low' | 'medium' | 'high' | 'urgent')[];
|
||||||
|
completed?: boolean;
|
||||||
|
dateRange?: 'overdue' | 'today' | 'tomorrow' | 'upcoming' | 'any';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface TodoAppSettings extends Record<string, unknown> {
|
export interface TodoAppSettings extends Record<string, unknown> {
|
||||||
// Task Behavior
|
// Task Behavior
|
||||||
|
|
@ -50,6 +61,10 @@ export interface TodoAppSettings extends Record<string, unknown> {
|
||||||
// Navigation UI
|
// Navigation UI
|
||||||
pillNavCollapsed: boolean;
|
pillNavCollapsed: boolean;
|
||||||
filterStripCollapsed: boolean;
|
filterStripCollapsed: boolean;
|
||||||
|
|
||||||
|
// Page mode
|
||||||
|
pageMode: PageMode;
|
||||||
|
customPages: PageConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: TodoAppSettings = {
|
const DEFAULT_SETTINGS: TodoAppSettings = {
|
||||||
|
|
@ -92,6 +107,10 @@ const DEFAULT_SETTINGS: TodoAppSettings = {
|
||||||
// Navigation UI
|
// Navigation UI
|
||||||
pillNavCollapsed: true, // PillNav hidden by default, shown via FAB
|
pillNavCollapsed: true, // PillNav hidden by default, shown via FAB
|
||||||
filterStripCollapsed: false, // FilterStrip shown by default when PillNav is visible
|
filterStripCollapsed: false, // FilterStrip shown by default when PillNav is visible
|
||||||
|
|
||||||
|
// Page mode
|
||||||
|
pageMode: 'priority' as PageMode,
|
||||||
|
customPages: [] as PageConfig[],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create base store using factory
|
// Create base store using factory
|
||||||
|
|
@ -183,6 +202,12 @@ export const todoSettings = {
|
||||||
get filterStripCollapsed() {
|
get filterStripCollapsed() {
|
||||||
return baseStore.settings.filterStripCollapsed;
|
return baseStore.settings.filterStripCollapsed;
|
||||||
},
|
},
|
||||||
|
get pageMode() {
|
||||||
|
return baseStore.settings.pageMode;
|
||||||
|
},
|
||||||
|
get customPages() {
|
||||||
|
return baseStore.settings.customPages;
|
||||||
|
},
|
||||||
|
|
||||||
// Toggle methods
|
// Toggle methods
|
||||||
togglePillNav() {
|
togglePillNav() {
|
||||||
|
|
|
||||||
|
|
@ -602,10 +602,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
max-width: 900px;
|
max-width: none;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 0.5rem;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-wrapper.full-width {
|
.content-wrapper.full-width {
|
||||||
|
|
@ -615,9 +615,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.content-wrapper {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.content-wrapper.full-width {
|
.content-wrapper.full-width {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
@ -625,19 +622,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.content-wrapper {
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
.content-wrapper.full-width {
|
.content-wrapper.full-width {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile: More space for QuickInputBar + PillNav */
|
/* Mobile: Space for QuickInputBar + PillNav */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
padding-bottom: calc(150px + env(safe-area-inset-bottom));
|
padding-bottom: calc(100px + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,19 @@
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
import { format, addDays, subDays, startOfDay } from 'date-fns';
|
import { format, addDays, subDays, startOfDay } from 'date-fns';
|
||||||
import { de } from 'date-fns/locale';
|
import { de } from 'date-fns/locale';
|
||||||
import { Sparkle, ArrowDown } from '@manacore/shared-icons';
|
import {
|
||||||
|
Sparkle,
|
||||||
|
ArrowDown,
|
||||||
|
Warning,
|
||||||
|
CalendarBlank,
|
||||||
|
CalendarDots,
|
||||||
|
CheckCircle,
|
||||||
|
} from '@manacore/shared-icons';
|
||||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||||
import { viewStore } from '$lib/stores/view.svelte';
|
import { viewStore } from '$lib/stores/view.svelte';
|
||||||
import { applyTaskFilters } from '$lib/utils/task-filters';
|
import { applyTaskFilters } from '$lib/utils/task-filters';
|
||||||
import { filterOverdue, filterToday, filterCompleted } from '$lib/data/task-queries';
|
import { filterOverdue, filterToday, filterCompleted } from '$lib/data/task-queries';
|
||||||
import TaskList from '$lib/components/TaskList.svelte';
|
import TaskList from '$lib/components/TaskList.svelte';
|
||||||
import CollapsibleSection from '$lib/components/CollapsibleSection.svelte';
|
|
||||||
import { TaskListSkeleton } from '$lib/components/skeletons';
|
import { TaskListSkeleton } from '$lib/components/skeletons';
|
||||||
import type { Task } from '@todo/shared';
|
import type { Task } from '@todo/shared';
|
||||||
|
|
||||||
|
|
@ -17,6 +23,7 @@
|
||||||
getContext('tasks');
|
getContext('tasks');
|
||||||
|
|
||||||
let tipDismissed = $state(false);
|
let tipDismissed = $state(false);
|
||||||
|
let completedOpen = $state(false);
|
||||||
|
|
||||||
// Stable date references (computed once, not on every re-render)
|
// Stable date references (computed once, not on every re-render)
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
|
|
@ -136,186 +143,433 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build pages array from visible sections
|
||||||
|
let pages = $derived.by(() => {
|
||||||
|
const p: { id: string; label: string; icon: string }[] = [];
|
||||||
|
if (overdueTasks.length > 0) p.push({ id: 'overdue', label: 'Überfällig', icon: 'warning' });
|
||||||
|
if (showTodaySection) p.push({ id: 'today', label: 'Heute', icon: 'calendar' });
|
||||||
|
if (showTomorrowSection) p.push({ id: 'tomorrow', label: 'Morgen', icon: 'calendar-dots' });
|
||||||
|
if (showUpcomingSection) p.push({ id: 'upcoming', label: 'Demnächst', icon: 'calendar-dots' });
|
||||||
|
if (showCompletedSection) p.push({ id: 'completed', label: 'Erledigt', icon: 'check' });
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
|
||||||
|
let activePage = $state(0);
|
||||||
|
let scrollContainer: HTMLDivElement | undefined = $state();
|
||||||
|
|
||||||
|
function scrollToPage(index: number) {
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
const sheets = scrollContainer.querySelectorAll('.notepad-sheet');
|
||||||
|
if (sheets[index]) {
|
||||||
|
sheets[index].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
if (!scrollContainer) return;
|
||||||
|
const sheets = scrollContainer.querySelectorAll('.notepad-sheet');
|
||||||
|
const containerRect = scrollContainer.getBoundingClientRect();
|
||||||
|
const center = containerRect.left + containerRect.width / 2;
|
||||||
|
|
||||||
|
let closest = 0;
|
||||||
|
let closestDist = Infinity;
|
||||||
|
sheets.forEach((sheet, i) => {
|
||||||
|
const rect = sheet.getBoundingClientRect();
|
||||||
|
const sheetCenter = rect.left + rect.width / 2;
|
||||||
|
const dist = Math.abs(sheetCenter - center);
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist;
|
||||||
|
closest = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
activePage = closest;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Todo</title>
|
<title>Todo</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="unified-view">
|
<svelte:window
|
||||||
{#if allTasks.loading}
|
onkeydown={(e) => {
|
||||||
<TaskListSkeleton sections={3} tasksPerSection={3} />
|
const target = e.target as HTMLElement;
|
||||||
{:else if allTasks.error}
|
const isInQuickInput = target.closest('.quick-input-bar');
|
||||||
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
|
if (isInQuickInput && (e.key === 'ArrowUp' || (e.key === 'Tab' && !e.shiftKey))) {
|
||||||
{allTasks.error}
|
const firstTitle = document.querySelector<HTMLElement>('.task-title[contenteditable]');
|
||||||
|
if (firstTitle) {
|
||||||
|
e.preventDefault();
|
||||||
|
firstTitle.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if allTasks.loading}
|
||||||
|
<div class="notepad-page">
|
||||||
|
<div class="scroll-track">
|
||||||
|
<div class="notepad-sheet">
|
||||||
|
<TaskListSkeleton sections={3} tasksPerSection={3} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if allEmpty}
|
</div>
|
||||||
<!-- Enhanced empty state -->
|
{:else if allTasks.error}
|
||||||
<div class="empty-state-container">
|
<div class="notepad-page">
|
||||||
<div class="empty-state-content">
|
<div class="scroll-track">
|
||||||
<div class="empty-state-icon">
|
<div class="notepad-sheet">
|
||||||
<Sparkle size={56} weight="duotone" />
|
<div class="bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 p-4 rounded-lg">
|
||||||
|
{allTasks.error}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<h2 class="empty-state-title">Bereit für einen produktiven Tag</h2>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="empty-state-cta">
|
{:else if allEmpty}
|
||||||
<p class="empty-state-cta-text">Tippe unten um loszulegen...</p>
|
<div class="notepad-page">
|
||||||
<div class="empty-state-arrow">
|
<div class="scroll-track">
|
||||||
<ArrowDown size={20} weight="bold" />
|
<div class="notepad-sheet">
|
||||||
|
<div class="empty-state-container">
|
||||||
|
<div class="empty-state-content">
|
||||||
|
<div class="empty-state-icon">
|
||||||
|
<Sparkle size={56} weight="duotone" />
|
||||||
|
</div>
|
||||||
|
<h2 class="empty-state-title">Bereit für einen produktiven Tag</h2>
|
||||||
|
<div class="empty-state-cta">
|
||||||
|
<p class="empty-state-cta-text">Tippe unten um loszulegen...</p>
|
||||||
|
<div class="empty-state-arrow">
|
||||||
|
<ArrowDown size={20} weight="bold" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty-state-examples">
|
||||||
|
<p class="examples-label">Schnellstart-Tipps</p>
|
||||||
|
<div class="examples-grid">
|
||||||
|
{#each syntaxExamples as example}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="example-chip"
|
||||||
|
onclick={() => handleExampleClick(example.text)}
|
||||||
|
title={example.description}
|
||||||
|
>
|
||||||
|
{example.text}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Page tabs -->
|
||||||
|
{#if pages.length > 1}
|
||||||
|
<div class="page-tabs">
|
||||||
|
{#each pages as page, i}
|
||||||
|
<button class="page-tab" class:active={activePage === i} onclick={() => scrollToPage(i)}>
|
||||||
|
{page.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="empty-state-examples">
|
<div class="notepad-page">
|
||||||
<p class="examples-label">Schnellstart-Tipps</p>
|
<div class="scroll-track" bind:this={scrollContainer} onscroll={handleScroll}>
|
||||||
<div class="examples-grid">
|
<!-- Overdue page -->
|
||||||
{#each syntaxExamples as example}
|
{#if overdueTasks.length > 0}
|
||||||
<button
|
<div class="notepad-sheet">
|
||||||
type="button"
|
<div class="sheet-header sheet-header-warning">
|
||||||
class="example-chip"
|
<Warning size={18} weight="bold" />
|
||||||
onclick={() => handleExampleClick(example.text)}
|
<span>Überfällig</span>
|
||||||
title={example.description}
|
<span class="section-count">{overdueTasks.length}</span>
|
||||||
>
|
</div>
|
||||||
{example.text}
|
<div class="sheet-content">
|
||||||
</button>
|
<TaskList
|
||||||
|
tasks={overdueTasks}
|
||||||
|
enableDragDrop
|
||||||
|
dropTargetDate="overdue"
|
||||||
|
onTaskDrop={handleTaskDrop}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Today page -->
|
||||||
|
{#if showTodaySection}
|
||||||
|
<div class="notepad-sheet">
|
||||||
|
<div class="sheet-header">
|
||||||
|
<CalendarBlank size={18} weight="bold" />
|
||||||
|
<span>Heute</span>
|
||||||
|
{#if todayTasks.length > 0}
|
||||||
|
<span class="section-count">{todayTasks.length}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="sheet-content">
|
||||||
|
<TaskList
|
||||||
|
tasks={todayTasks}
|
||||||
|
enableDragDrop
|
||||||
|
dropTargetDate={today}
|
||||||
|
onTaskDrop={handleTaskDrop}
|
||||||
|
/>
|
||||||
|
{#if showOnboardingTip && !tipDismissed}
|
||||||
|
<div class="onboarding-tip">
|
||||||
|
<span class="onboarding-tip-icon">💡</span>
|
||||||
|
<span class="onboarding-tip-text">
|
||||||
|
Tipp: Nutze <code>#tags</code> und <code>!priorität</code> für bessere Organisation
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="onboarding-tip-close"
|
||||||
|
onclick={() => (tipDismissed = true)}
|
||||||
|
title="Tipp ausblenden"
|
||||||
|
aria-label="Tipp ausblenden"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Tomorrow page -->
|
||||||
|
{#if showTomorrowSection}
|
||||||
|
<div class="notepad-sheet">
|
||||||
|
<div class="sheet-header">
|
||||||
|
<CalendarDots size={18} weight="bold" />
|
||||||
|
<span>Morgen</span>
|
||||||
|
<span class="section-count">{tomorrowTasks.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sheet-content">
|
||||||
|
<TaskList
|
||||||
|
tasks={tomorrowTasks}
|
||||||
|
enableDragDrop
|
||||||
|
dropTargetDate={tomorrow}
|
||||||
|
onTaskDrop={handleTaskDrop}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Upcoming page -->
|
||||||
|
{#if showUpcomingSection}
|
||||||
|
<div class="notepad-sheet">
|
||||||
|
<div class="sheet-header">
|
||||||
|
<CalendarDots size={18} weight="bold" />
|
||||||
|
<span>Demnächst</span>
|
||||||
|
<span class="section-count">{upcomingCount}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sheet-content">
|
||||||
|
{#each groupedUpcomingTasks as group}
|
||||||
|
<div class="subsection">
|
||||||
|
<h3 class="subsection-label">{group.label}</h3>
|
||||||
|
<TaskList
|
||||||
|
tasks={group.tasks}
|
||||||
|
enableDragDrop
|
||||||
|
dropTargetDate={group.date}
|
||||||
|
onTaskDrop={handleTaskDrop}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="notepad">
|
|
||||||
<div class="notepad-content">
|
|
||||||
<div class="space-y-2">
|
|
||||||
{#if overdueTasks.length > 0}
|
|
||||||
<CollapsibleSection
|
|
||||||
title="Überfällig"
|
|
||||||
count={overdueTasks.length}
|
|
||||||
icon="warning"
|
|
||||||
variant="warning"
|
|
||||||
defaultOpen={true}
|
|
||||||
>
|
|
||||||
<TaskList
|
|
||||||
tasks={overdueTasks}
|
|
||||||
enableDragDrop
|
|
||||||
dropTargetDate="overdue"
|
|
||||||
onTaskDrop={handleTaskDrop}
|
|
||||||
/>
|
|
||||||
</CollapsibleSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showTodaySection}
|
<!-- Completed page -->
|
||||||
<CollapsibleSection
|
{#if showCompletedSection}
|
||||||
title="Heute"
|
<div class="notepad-sheet sheet-completed">
|
||||||
count={todayTasks.length}
|
<div class="sheet-header">
|
||||||
icon="today"
|
<CheckCircle size={18} weight="bold" />
|
||||||
variant="default"
|
<span>Erledigt</span>
|
||||||
defaultOpen={true}
|
<span class="section-count">{completedTasks.length}</span>
|
||||||
>
|
</div>
|
||||||
<TaskList
|
<div class="sheet-content">
|
||||||
tasks={todayTasks}
|
<TaskList
|
||||||
enableDragDrop
|
tasks={completedTasks}
|
||||||
dropTargetDate={today}
|
enableDragDrop
|
||||||
onTaskDrop={handleTaskDrop}
|
dropTargetDate="completed"
|
||||||
/>
|
onTaskDrop={handleTaskDrop}
|
||||||
</CollapsibleSection>
|
showCompleted
|
||||||
{/if}
|
/>
|
||||||
|
</div>
|
||||||
{#if showTomorrowSection}
|
|
||||||
<CollapsibleSection
|
|
||||||
title="Morgen"
|
|
||||||
count={tomorrowTasks.length}
|
|
||||||
icon="upcoming"
|
|
||||||
variant="default"
|
|
||||||
defaultOpen={true}
|
|
||||||
>
|
|
||||||
<TaskList
|
|
||||||
tasks={tomorrowTasks}
|
|
||||||
enableDragDrop
|
|
||||||
dropTargetDate={tomorrow}
|
|
||||||
onTaskDrop={handleTaskDrop}
|
|
||||||
/>
|
|
||||||
</CollapsibleSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showUpcomingSection}
|
|
||||||
<CollapsibleSection
|
|
||||||
title="Demnächst"
|
|
||||||
count={upcomingCount}
|
|
||||||
icon="upcoming"
|
|
||||||
variant="default"
|
|
||||||
defaultOpen={true}
|
|
||||||
>
|
|
||||||
<div class="space-y-4">
|
|
||||||
{#each groupedUpcomingTasks as group}
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-medium text-muted-foreground mb-2 pl-2">
|
|
||||||
{group.label}
|
|
||||||
</h3>
|
|
||||||
<TaskList
|
|
||||||
tasks={group.tasks}
|
|
||||||
enableDragDrop
|
|
||||||
dropTargetDate={group.date}
|
|
||||||
onTaskDrop={handleTaskDrop}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</CollapsibleSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showCompletedSection}
|
|
||||||
<CollapsibleSection
|
|
||||||
title="Erledigt"
|
|
||||||
count={completedTasks.length}
|
|
||||||
icon="completed"
|
|
||||||
variant="success"
|
|
||||||
defaultOpen={true}
|
|
||||||
>
|
|
||||||
<TaskList
|
|
||||||
tasks={completedTasks}
|
|
||||||
enableDragDrop
|
|
||||||
dropTargetDate="completed"
|
|
||||||
onTaskDrop={handleTaskDrop}
|
|
||||||
showCompleted
|
|
||||||
/>
|
|
||||||
</CollapsibleSection>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showOnboardingTip && !tipDismissed}
|
|
||||||
<div class="onboarding-tip">
|
|
||||||
<span class="onboarding-tip-icon">💡</span>
|
|
||||||
<span class="onboarding-tip-text">
|
|
||||||
Tipp: Nutze <code>#tags</code> und <code>!priorität</code> für bessere Organisation
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="onboarding-tip-close"
|
|
||||||
onclick={() => (tipDismissed = true)}
|
|
||||||
title="Tipp ausblenden"
|
|
||||||
aria-label="Tipp ausblenden"
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.unified-view {
|
/* ── Page tabs ── */
|
||||||
|
.page-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.5rem 1.5rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.page-tabs::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-tab {
|
||||||
|
padding: 0.375rem 0.875rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-tab:hover {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-tab.active {
|
||||||
|
color: hsl(var(--color-primary));
|
||||||
|
border-bottom-color: hsl(var(--color-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .page-tab {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
:global(.dark) .page-tab:hover {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
:global(.dark) .page-tab.active {
|
||||||
|
color: hsl(var(--color-primary-light, var(--color-primary)));
|
||||||
|
border-bottom-color: hsl(var(--color-primary-light, var(--color-primary)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Notepad page — horizontal scroll wrapper ── */
|
||||||
|
.notepad-page {
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-track {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-padding: 1.5rem;
|
||||||
|
padding: 1rem 1.5rem 2rem;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.scroll-track::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Paper sheet ── */
|
||||||
|
.notepad-sheet {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: min(840px, 85vw);
|
||||||
|
min-height: 60vh;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
background: #fffef5;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.08),
|
||||||
|
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .notepad-sheet {
|
||||||
|
background-color: #252220;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-completed {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sheet header ── */
|
||||||
|
.sheet-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .sheet-header {
|
||||||
|
color: #d1d5db;
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sheet-header-warning {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
:global(.dark) .sheet-header-warning {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
color: #6b7280;
|
||||||
|
padding: 0.0625rem 0.4375rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
:global(.dark) .section-count {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.sheet-header-warning .section-count {
|
||||||
|
background: rgba(220, 38, 38, 0.1);
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
:global(.dark) .sheet-header-warning .section-count {
|
||||||
|
background: rgba(248, 113, 113, 0.15);
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sheet content ── */
|
||||||
|
.sheet-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Subsections (upcoming days) ── */
|
||||||
|
.subsection {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subsection-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9ca3af;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
:global(.dark) .subsection-label {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty state ── */
|
||||||
.empty-state-container {
|
.empty-state-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -350,9 +604,12 @@
|
||||||
.empty-state-title {
|
.empty-state-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: hsl(var(--color-foreground));
|
color: #374151;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
:global(.dark) .empty-state-title {
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state-cta {
|
.empty-state-cta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -360,15 +617,21 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
background: hsl(var(--color-surface-hover));
|
background: rgba(0, 0, 0, 0.04);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
:global(.dark) .empty-state-cta {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state-cta-text {
|
.empty-state-cta-text {
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: #6b7280;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
}
|
}
|
||||||
|
:global(.dark) .empty-state-cta-text {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state-arrow {
|
.empty-state-arrow {
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
|
|
@ -396,7 +659,7 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examples-grid {
|
.examples-grid {
|
||||||
|
|
@ -410,34 +673,35 @@
|
||||||
padding: 0.5rem 0.875rem;
|
padding: 0.5rem 0.875rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||||
background: hsl(var(--color-surface));
|
background: rgba(255, 255, 255, 0.6);
|
||||||
border: 1px solid hsl(var(--color-border));
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
color: hsl(var(--color-foreground));
|
color: #374151;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
:global(.dark) .example-chip {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.12);
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
.example-chip:hover {
|
.example-chip:hover {
|
||||||
background: hsl(var(--color-primary) / 0.1);
|
background: hsl(var(--color-primary) / 0.1);
|
||||||
border-color: hsl(var(--color-primary));
|
border-color: hsl(var(--color-primary));
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.example-chip:active {
|
.example-chip:active {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Onboarding tip ── */
|
||||||
.onboarding-tip {
|
.onboarding-tip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
padding: 0.875rem 1rem;
|
padding: 0.875rem 1.5rem;
|
||||||
margin-top: 1rem;
|
margin: 0.5rem 0;
|
||||||
background: hsl(var(--color-primary) / 0.08);
|
|
||||||
border: 1px solid hsl(var(--color-primary) / 0.2);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,7 +710,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-tip-text {
|
.onboarding-tip-text {
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
:global(.dark) .onboarding-tip-text {
|
||||||
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-tip-close {
|
.onboarding-tip-close {
|
||||||
|
|
@ -454,17 +721,19 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
color: hsl(var(--color-muted-foreground));
|
color: #9ca3af;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.onboarding-tip-close:hover {
|
.onboarding-tip-close:hover {
|
||||||
color: hsl(var(--color-foreground));
|
color: #374151;
|
||||||
background: hsl(var(--color-primary) / 0.15);
|
background: hsl(var(--color-primary) / 0.15);
|
||||||
}
|
}
|
||||||
|
:global(.dark) .onboarding-tip-close:hover {
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
.onboarding-tip-text code {
|
.onboarding-tip-text code {
|
||||||
padding: 0.125rem 0.375rem;
|
padding: 0.125rem 0.375rem;
|
||||||
|
|
@ -474,34 +743,4 @@
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
color: hsl(var(--color-primary));
|
color: hsl(var(--color-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.notepad {
|
|
||||||
max-width: 560px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: #fffef5;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
0 2px 8px rgba(0, 0, 0, 0.08),
|
|
||||||
0 1px 2px rgba(0, 0, 0, 0.04);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .notepad {
|
|
||||||
background: #2a2520;
|
|
||||||
box-shadow:
|
|
||||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
|
||||||
0 1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notepad-content {
|
|
||||||
padding: 0.75rem 1rem 1.5rem 1rem;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.notepad {
|
|
||||||
max-width: 100%;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { isLoading as i18nLoading } from 'svelte-i18n';
|
import { isLoading as i18nLoading } from 'svelte-i18n';
|
||||||
import { theme } from '$lib/stores/theme';
|
import { theme } from '$lib/stores/theme';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
import { authStore } from '$lib/stores/auth.svelte';
|
||||||
|
import { debugStore } from '$lib/stores/debug.svelte';
|
||||||
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
import { AppLoadingSkeleton } from '$lib/components/skeletons';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
@ -13,14 +14,20 @@
|
||||||
|
|
||||||
let appReady = $derived(!loading && !$i18nLoading);
|
let appReady = $derived(!loading && !$i18nLoading);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
// Initialize theme
|
|
||||||
theme.initialize();
|
theme.initialize();
|
||||||
|
authStore.initialize().then(() => {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize auth
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
await authStore.initialize();
|
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
||||||
|
e.preventDefault();
|
||||||
loading = false;
|
debugStore.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKey);
|
||||||
|
return () => window.removeEventListener('keydown', handleKey);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -40,7 +47,16 @@
|
||||||
{#if !appReady}
|
{#if !appReady}
|
||||||
<AppLoadingSkeleton layout="tasks" listItemCount={4} />
|
<AppLoadingSkeleton layout="tasks" listItemCount={4} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="min-h-screen bg-background text-foreground">
|
<div class="min-h-screen bg-background text-foreground" class:debug-mode={debugStore.enabled}>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if debugStore.enabled}
|
||||||
|
<button
|
||||||
|
class="fixed top-2 right-2 z-[9999] rounded bg-red-600 px-2 py-0.5 font-mono text-xs text-white opacity-80 hover:opacity-100"
|
||||||
|
onclick={() => debugStore.toggle()}
|
||||||
|
>
|
||||||
|
DEBUG
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue