feat(guides): Phase 2 — step editor, edit mode, collection management

Guide detail page:
- Edit mode toggle (✏ Bearbeiten / ✓ Fertig)
- StepEditorModal: 5 step types, title, markdown content, checkable toggle
- Per-step controls in edit mode: edit, delete, reorder ↑↓ (on hover)
- Section management: add / delete sections in edit mode
- Unsectioned + sectioned steps both editable
- Empty state with CTA to enter edit mode
- Guide delete with confirmation dialog
- Run history shows mode icon (📜 scroll / 🎯 focus)

Collections page:
- CollectionEditModal: emoji, color, path vs library type
- Create collection from empty state and header button
- Edit/delete existing collections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 21:23:47 +02:00
parent 8e496ff417
commit 1a999f8cca
5 changed files with 688 additions and 111 deletions

View file

@ -151,7 +151,15 @@ apps/guides/apps/web/src/routes/
- [x] GuideEditModal
- [x] Registrierung in mana-apps.ts + app-icons.ts
### Phase 2 — Web-Import & Sharing
### Phase 2 — Content-Editing ✅ Abgeschlossen
- [x] StepEditorModal (Typ, Titel, Content, Checkable-Toggle)
- [x] Guide-Detail Edit-Mode (Steps hinzufügen, bearbeiten, löschen, sortieren ↑↓)
- [x] Abschnitt-Verwaltung im Edit-Mode (hinzufügen, löschen)
- [x] Guide löschen (mit Bestätigungs-Dialog)
- [x] CollectionEditModal (Emoji, Farbe, Typ path/library)
- [x] Collections erstellen/bearbeiten aus Collections-View
### Phase 3 — Web-Import & Sharing
- [ ] Hono/Bun-Server (apps/guides/apps/server/)
- [ ] Web-Import: URL → Guide via mana-search
- [ ] Guide-Export: JSON / Markdown

View file

@ -0,0 +1,142 @@
<script lang="ts">
import type { LocalCollection } from '$lib/data/local-store.js';
import type { BaseRecord } from '@manacore/local-store';
type CollectionInput = Omit<LocalCollection, keyof BaseRecord>;
interface Props {
open: boolean;
collection?: LocalCollection;
onClose: () => void;
onSave: (data: CollectionInput) => Promise<void>;
onDelete?: (id: string) => Promise<void>;
}
let { open, collection, onClose, onSave, onDelete }: Props = $props();
let title = $state(collection?.title ?? '');
let description = $state(collection?.description ?? '');
let coverEmoji = $state(collection?.coverEmoji ?? '📚');
let coverColor = $state(collection?.coverColor ?? '#0d9488');
let type = $state<'path' | 'library'>(collection?.type ?? 'path');
let saving = $state(false);
$effect(() => {
if (collection) {
title = collection.title;
description = collection.description ?? '';
coverEmoji = collection.coverEmoji ?? '📚';
coverColor = collection.coverColor ?? '#0d9488';
type = collection.type;
}
});
const COVER_COLORS = ['#0d9488', '#3b82f6', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#10b981', '#f97316'];
async function handleSave() {
if (!title.trim()) return;
saving = true;
try {
await onSave({
title: title.trim(),
description: description.trim() || undefined,
coverEmoji,
coverColor,
type,
guideOrder: collection?.guideOrder ?? [],
});
} finally {
saving = false;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') handleSave();
}
function handleBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) onClose();
}
</script>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="fixed inset-0 z-50 flex items-end justify-center bg-black/50 sm:items-center sm:p-4" onmousedown={handleBackdrop}>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div role="dialog" aria-modal="true" class="w-full max-w-md overflow-hidden rounded-t-2xl bg-background shadow-xl sm:rounded-2xl" onkeydown={handleKeydown}>
<div class="flex items-center justify-between border-b border-border px-5 py-4">
<h2 class="font-semibold text-foreground">{collection ? 'Sammlung bearbeiten' : 'Neue Sammlung'}</h2>
<button onclick={onClose} class="text-muted-foreground hover:text-foreground"></button>
</div>
<div class="p-5 space-y-4">
<!-- Emoji + Color -->
<div class="flex items-center gap-4">
<div>
<p class="mb-1 text-xs text-muted-foreground">Emoji</p>
<input type="text" bind:value={coverEmoji} maxlength="2"
class="h-12 w-16 rounded-xl border border-border bg-surface text-center text-2xl focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div>
<div class="flex-1">
<p class="mb-1 text-xs text-muted-foreground">Farbe</p>
<div class="flex gap-2 flex-wrap">
{#each COVER_COLORS as color}
<button onclick={() => (coverColor = color)}
class="h-7 w-7 rounded-full transition-transform hover:scale-110 {coverColor === color ? 'ring-2 ring-offset-2 ring-foreground' : ''}"
style="background-color: {color}"></button>
{/each}
</div>
</div>
</div>
<!-- Title -->
<div>
<label class="mb-1 block text-xs font-medium text-muted-foreground">Titel *</label>
<input type="text" bind:value={title} placeholder="z.B. Developer Starter Kit" autofocus
class="w-full rounded-xl border border-border bg-surface px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" />
</div>
<!-- Description -->
<div>
<label class="mb-1 block text-xs font-medium text-muted-foreground">Beschreibung</label>
<textarea bind:value={description} rows="2" placeholder="Kurze Beschreibung..."
class="w-full resize-none rounded-xl border border-border bg-surface px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"></textarea>
</div>
<!-- Type -->
<div>
<label class="mb-2 block text-xs font-medium text-muted-foreground">Typ</label>
<div class="grid grid-cols-2 gap-2">
<button onclick={() => (type = 'path')}
class="rounded-xl border p-3 text-left transition-colors {type === 'path' ? 'border-primary bg-primary/10' : 'border-border hover:bg-accent'}">
<div class="text-lg mb-1">🗺</div>
<div class="text-sm font-medium text-foreground">Lernpfad</div>
<div class="text-xs text-muted-foreground">Geordnete Sequenz</div>
</button>
<button onclick={() => (type = 'library')}
class="rounded-xl border p-3 text-left transition-colors {type === 'library' ? 'border-primary bg-primary/10' : 'border-border hover:bg-accent'}">
<div class="text-lg mb-1">📚</div>
<div class="text-sm font-medium text-foreground">Bibliothek</div>
<div class="text-xs text-muted-foreground">Thematische Gruppe</div>
</button>
</div>
</div>
</div>
<div class="flex items-center justify-between border-t border-border px-5 py-4">
<div>
{#if collection && onDelete}
<button onclick={() => onDelete(collection!.id)} class="text-sm text-red-500 hover:text-red-600">Löschen</button>
{/if}
</div>
<div class="flex gap-3">
<button onclick={onClose} class="rounded-xl border border-border px-4 py-2 text-sm hover:bg-accent">Abbrechen</button>
<button onclick={handleSave} disabled={!title.trim() || saving}
class="rounded-xl bg-primary px-5 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50">
{saving ? 'Speichern...' : collection ? 'Speichern' : 'Erstellen'}
</button>
</div>
</div>
</div>
</div>
{/if}

View file

@ -0,0 +1,171 @@
<script lang="ts">
import type { LocalStep, StepType } from '$lib/data/local-store.js';
import type { BaseRecord } from '@manacore/local-store';
type StepInput = Omit<LocalStep, keyof BaseRecord>;
interface Props {
open: boolean;
step?: LocalStep;
guideId: string;
sectionId?: string;
order: number;
onClose: () => void;
onSave: (data: StepInput) => Promise<void>;
}
let { open, step, guideId, sectionId, order, onClose, onSave }: Props = $props();
let title = $state(step?.title ?? '');
let content = $state(step?.content ?? '');
let type = $state<StepType>(step?.type ?? 'instruction');
let checkable = $state(step?.checkable ?? true);
let saving = $state(false);
$effect(() => {
if (step) {
title = step.title;
content = step.content ?? '';
type = step.type;
checkable = step.checkable;
} else {
title = '';
content = '';
type = 'instruction';
checkable = true;
}
});
const STEP_TYPES: { value: StepType; label: string; icon: string; description: string }[] = [
{ value: 'instruction', label: 'Anweisung', icon: '→', description: 'Normaler Schritt' },
{ value: 'warning', label: 'Warnung', icon: '⚠', description: 'Wichtiger Hinweis' },
{ value: 'tip', label: 'Tipp', icon: '💡', description: 'Hilfreicher Hinweis' },
{ value: 'checkpoint', label: 'Checkpoint', icon: '✓', description: 'Überprüfungspunkt' },
{ value: 'code', label: 'Code', icon: '</>', description: 'Code-Block (Markdown)' },
];
async function handleSave() {
if (!title.trim()) return;
saving = true;
try {
await onSave({ title: title.trim(), content: content.trim() || undefined, type, checkable, guideId, sectionId, order });
} finally {
saving = false;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') handleSave();
}
function handleBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) onClose();
}
</script>
{#if open}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-end justify-center bg-black/50 sm:items-center sm:p-4"
onmousedown={handleBackdrop}
>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
role="dialog"
aria-modal="true"
aria-label={step ? 'Schritt bearbeiten' : 'Neuer Schritt'}
class="w-full max-w-lg overflow-hidden rounded-t-2xl bg-background shadow-xl sm:rounded-2xl"
onkeydown={handleKeydown}
>
<div class="flex items-center justify-between border-b border-border px-5 py-4">
<h2 class="font-semibold text-foreground">{step ? 'Schritt bearbeiten' : 'Neuer Schritt'}</h2>
<button onclick={onClose} class="text-muted-foreground hover:text-foreground"></button>
</div>
<div class="max-h-[75vh] overflow-y-auto p-5 space-y-4">
<!-- Step type selector -->
<div>
<label class="mb-2 block text-xs font-medium text-muted-foreground">Typ</label>
<div class="grid grid-cols-5 gap-1.5">
{#each STEP_TYPES as t}
<button
onclick={() => (type = t.value)}
title={t.description}
class="flex flex-col items-center gap-1 rounded-xl border py-2.5 text-center transition-colors
{type === t.value
? 'border-primary bg-primary/10 text-primary'
: 'border-border text-muted-foreground hover:bg-accent'}"
>
<span class="text-base">{t.icon}</span>
<span class="text-[10px] leading-tight">{t.label}</span>
</button>
{/each}
</div>
</div>
<!-- Title -->
<div>
<label class="mb-1 block text-xs font-medium text-muted-foreground">Schritt-Titel *</label>
<input
type="text"
bind:value={title}
placeholder="z.B. npm install ausführen"
autofocus
class="w-full rounded-xl border border-border bg-surface px-3 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
</div>
<!-- Content -->
<div>
<label class="mb-1 block text-xs font-medium text-muted-foreground">
Beschreibung / Inhalt
{#if type === 'code'}
<span class="font-normal">(Markdown · ```bash ... ``` für Code-Blöcke)</span>
{/if}
</label>
<textarea
bind:value={content}
placeholder={type === 'code'
? '```bash\nnpm install\nnpm run dev\n```'
: 'Optionale Beschreibung oder Details...'}
rows={type === 'code' ? 5 : 3}
class="w-full resize-y rounded-xl border border-border bg-surface px-3 py-2.5 font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
></textarea>
</div>
<!-- Checkable toggle -->
<label class="flex cursor-pointer items-center justify-between rounded-xl border border-border bg-surface px-4 py-3">
<div>
<p class="text-sm font-medium text-foreground">Abhakbar</p>
<p class="text-xs text-muted-foreground">Schritt kann im Run-Modus abgehakt werden</p>
</div>
<div
class="relative h-5 w-9 rounded-full transition-colors {checkable ? 'bg-primary' : 'bg-muted'}"
onclick={() => (checkable = !checkable)}
role="switch"
aria-checked={checkable}
tabindex="0"
>
<span
class="absolute top-0.5 h-4 w-4 rounded-full bg-white shadow transition-transform {checkable ? 'translate-x-4' : 'translate-x-0.5'}"
></span>
</div>
</label>
</div>
<div class="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
<button onclick={onClose} class="rounded-xl border border-border px-4 py-2 text-sm text-foreground hover:bg-accent">
Abbrechen
</button>
<button
onclick={handleSave}
disabled={!title.trim() || saving}
class="rounded-xl bg-primary px-5 py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-50"
>
{saving ? 'Speichern...' : step ? 'Speichern' : 'Hinzufügen'}
</button>
</div>
</div>
</div>
{/if}

View file

@ -2,6 +2,11 @@
import { liveQuery } from 'dexie';
import { collectionCollection, guideCollection, runCollection } from '$lib/data/local-store.js';
import type { LocalCollection, LocalGuide, LocalRun } from '$lib/data/local-store.js';
import { guidesStore } from '$lib/stores/guides.svelte';
import CollectionEditModal from '$lib/components/CollectionEditModal.svelte';
let showCreateModal = $state(false);
let editingCollection = $state<LocalCollection | undefined>(undefined);
let collections = $state<LocalCollection[]>([]);
let allGuides = $state<LocalGuide[]>([]);
@ -43,18 +48,32 @@
</script>
<div class="p-4 md:p-6">
<div class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Sammlungen</h1>
<p class="text-sm text-muted-foreground">Lernpfade und thematische Anleitungs-Sets</p>
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-foreground">Sammlungen</h1>
<p class="text-sm text-muted-foreground">Lernpfade und thematische Anleitungs-Sets</p>
</div>
<button
onclick={() => { editingCollection = undefined; showCreateModal = true; }}
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
>
+ Neu
</button>
</div>
{#if collections.length === 0}
<div class="flex flex-col items-center justify-center py-24 text-center">
<span class="mb-4 text-6xl">📂</span>
<h2 class="mb-2 text-lg font-semibold">Noch keine Sammlungen</h2>
<p class="text-sm text-muted-foreground max-w-sm">
<p class="mb-6 text-sm text-muted-foreground max-w-sm">
Sammlungen gruppieren Anleitungen zu Lernpfaden oder thematischen Bibliotheken.
</p>
<button
onclick={() => { editingCollection = undefined; showCreateModal = true; }}
class="rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-white hover:bg-primary-hover"
>
Erste Sammlung erstellen
</button>
</div>
{:else}
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@ -105,3 +124,20 @@
</div>
{/if}
</div>
{#if showCreateModal}
<CollectionEditModal
open={true}
collection={editingCollection}
onClose={() => (showCreateModal = false)}
onSave={async (data) => {
if (editingCollection) await guidesStore.updateCollection(editingCollection.id, data);
else await guidesStore.createCollection(data);
showCreateModal = false;
}}
onDelete={editingCollection ? async (id) => {
await collectionCollection.delete(id);
showCreateModal = false;
} : undefined}
/>
{/if}

View file

@ -14,15 +14,18 @@
} from '$lib/data/local-store.js';
import { runsStore } from '$lib/stores/runs.svelte';
import { guidesStore } from '$lib/stores/guides.svelte';
import GuideEditModal from '$lib/components/GuideEditModal.svelte';
import StepEditorModal from '$lib/components/StepEditorModal.svelte';
import type { BaseRecord } from '@manacore/local-store';
let guideId = $derived($page.params.id);
// ── Data ────────────────────────────────────────────────
let guide = $state<LocalGuide | null>(null);
let sections = $state<LocalSection[]>([]);
let steps = $state<LocalStep[]>([]);
let runs = $state<LocalRun[]>([]);
let showEditModal = $state(false);
let showDeleteConfirm = $state(false);
$effect(() => {
const id = guideId;
@ -43,22 +46,94 @@
return () => sub.unsubscribe();
});
// ── Derived ─────────────────────────────────────────────
let completedRuns = $derived(runs.filter((r) => r.completedAt));
let activeRun = $derived(runs.find((r) => !r.completedAt));
let totalSteps = $derived(steps.filter((s) => s.checkable).length);
let checkableSteps = $derived(steps.filter((s) => s.checkable));
function getStepsForSection(sectionId: string) {
return steps.filter((s) => s.sectionId === sectionId);
return steps.filter((s) => s.sectionId === sectionId).sort((a, b) => a.order - b.order);
}
function getUnsectionedSteps() {
return steps.filter((s) => !s.sectionId);
return steps.filter((s) => !s.sectionId).sort((a, b) => a.order - b.order);
}
function getActiveRunProgress() {
if (!activeRun || !checkableSteps.length) return 0;
const done = Object.values(activeRun.stepStates).filter((s) => s.done).length;
return Math.round((done / checkableSteps.length) * 100);
}
function getActiveRunProgress() {
if (!activeRun) return 0;
const done = Object.values(activeRun.stepStates).filter((s) => s.done).length;
return totalSteps > 0 ? Math.round((done / totalSteps) * 100) : 0;
// ── UI state ────────────────────────────────────────────
let editMode = $state(false);
let showGuideEditModal = $state(false);
let showDeleteConfirm = $state(false);
let showAddSection = $state(false);
let newSectionTitle = $state('');
// StepEditorModal state
let stepModalOpen = $state(false);
let editingStep = $state<LocalStep | undefined>(undefined);
let stepModalSectionId = $state<string | undefined>(undefined);
function openAddStep(sectionId?: string) {
editingStep = undefined;
stepModalSectionId = sectionId;
stepModalOpen = true;
}
function openEditStep(step: LocalStep) {
editingStep = step;
stepModalSectionId = step.sectionId;
stepModalOpen = true;
}
// ── Actions ─────────────────────────────────────────────
async function handleSaveStep(data: Omit<LocalStep, keyof BaseRecord>) {
if (editingStep) {
await guidesStore.updateStep(editingStep.id, data);
} else {
const targetSteps = data.sectionId
? getStepsForSection(data.sectionId)
: getUnsectionedSteps();
await guidesStore.createStep({ ...data, order: targetSteps.length });
}
stepModalOpen = false;
}
async function deleteStep(stepId: string) {
await guidesStore.deleteStep(stepId);
}
async function moveStep(step: LocalStep, direction: 'up' | 'down') {
const siblings = step.sectionId
? getStepsForSection(step.sectionId)
: getUnsectionedSteps();
const idx = siblings.findIndex((s) => s.id === step.id);
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
if (swapIdx < 0 || swapIdx >= siblings.length) return;
const other = siblings[swapIdx];
await Promise.all([
guidesStore.updateStep(step.id, { order: other.order }),
guidesStore.updateStep(other.id, { order: step.order }),
]);
}
async function addSection() {
if (!newSectionTitle.trim()) return;
await guidesStore.createSection({ guideId, title: newSectionTitle.trim(), order: sections.length });
newSectionTitle = '';
showAddSection = false;
}
async function deleteSection(sectionId: string) {
await guidesStore.deleteSection(sectionId);
}
async function handleDeleteGuide() {
await guidesStore.deleteGuide(guideId);
goto('/');
}
async function startRun(mode: 'scroll' | 'focus') {
@ -70,97 +145,115 @@
if (activeRun) goto(`/guide/${guideId}/run?runId=${activeRun.id}&mode=${activeRun.mode}`);
}
// ── Display config ───────────────────────────────────────
const difficultyConfig = {
easy: { label: 'Einfach', color: 'text-green-600 bg-green-50' },
medium: { label: 'Mittel', color: 'text-amber-600 bg-amber-50' },
hard: { label: 'Schwer', color: 'text-red-600 bg-red-50' },
easy: { label: 'Einfach', color: 'text-green-600 bg-green-50 dark:bg-green-950/30' },
medium: { label: 'Mittel', color: 'text-amber-600 bg-amber-50 dark:bg-amber-950/30' },
hard: { label: 'Schwer', color: 'text-red-600 bg-red-50 dark:bg-red-950/30' },
};
const stepTypeConfig = {
instruction: { icon: '→', color: 'border-l-primary' },
warning: { icon: '⚠', color: 'border-l-orange-400' },
tip: { icon: '💡', color: 'border-l-violet-400' },
checkpoint: { icon: '✓', color: 'border-l-blue-400' },
code: { icon: '</>', color: 'border-l-slate-400' },
const stepTypeConfig: Record<string, { icon: string; border: string; bg: string }> = {
instruction: { icon: '→', border: 'border-l-primary', bg: '' },
warning: { icon: '⚠', border: 'border-l-orange-400', bg: 'bg-orange-50/50 dark:bg-orange-950/20' },
tip: { icon: '💡', border: 'border-l-violet-400', bg: 'bg-violet-50/50 dark:bg-violet-950/20' },
checkpoint: { icon: '✓', border: 'border-l-blue-400', bg: 'bg-blue-50/50 dark:bg-blue-950/20' },
code: { icon: '</>', border: 'border-l-slate-400', bg: 'bg-slate-50/50 dark:bg-slate-950/20' },
};
</script>
{#if !guide}
<div class="flex h-full items-center justify-center">
<div class="flex h-full items-center justify-center p-8">
<p class="text-muted-foreground">Anleitung nicht gefunden.</p>
</div>
{:else}
<div class="mx-auto max-w-3xl p-4 md:p-8">
<!-- Back -->
<a href="/" class="mb-6 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
← Bibliothek
</a>
<!-- Back + Edit toggle -->
<div class="mb-5 flex items-center justify-between">
<a href="/" class="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
← Bibliothek
</a>
<div class="flex items-center gap-2">
<button
onclick={() => (editMode = !editMode)}
class="rounded-lg px-3 py-1.5 text-xs font-medium transition-colors
{editMode ? 'bg-primary text-white' : 'border border-border text-muted-foreground hover:bg-accent'}"
>
{editMode ? '✓ Fertig' : '✏ Bearbeiten'}
</button>
</div>
</div>
<!-- Cover header -->
<div
class="mb-6 flex items-center gap-4 rounded-2xl p-6"
style="background-color: {guide.coverColor ?? '#0d9488'}20"
class="mb-6 rounded-2xl p-5"
style="background-color: {guide.coverColor ?? '#0d9488'}18"
>
<span class="text-5xl">{guide.coverEmoji ?? '📖'}</span>
<div class="flex-1 min-w-0">
<h1 class="text-2xl font-bold text-foreground">{guide.title}</h1>
{#if guide.description}
<p class="mt-1 text-sm text-muted-foreground">{guide.description}</p>
{/if}
<div class="mt-3 flex flex-wrap items-center gap-2">
<span class="rounded-full px-2 py-0.5 text-xs font-medium {difficultyConfig[guide.difficulty].color}">
{difficultyConfig[guide.difficulty].label}
</span>
{#if guide.estimatedMinutes}
<span class="text-xs text-muted-foreground">{guide.estimatedMinutes} min</span>
{/if}
<span class="text-xs text-muted-foreground">{totalSteps} Schritte</span>
{#if completedRuns.length > 0}
<span class="text-xs text-muted-foreground">{completedRuns.length}× abgeschlossen</span>
<div class="flex items-start gap-4">
<span class="flex-shrink-0 text-5xl">{guide.coverEmoji ?? '📖'}</span>
<div class="flex-1 min-w-0">
<h1 class="text-xl font-bold text-foreground leading-tight">{guide.title}</h1>
{#if guide.description}
<p class="mt-1 text-sm text-muted-foreground">{guide.description}</p>
{/if}
<div class="mt-2 flex flex-wrap items-center gap-2">
<span class="rounded-full px-2 py-0.5 text-xs font-medium {difficultyConfig[guide.difficulty].color}">
{difficultyConfig[guide.difficulty].label}
</span>
{#if guide.estimatedMinutes}
<span class="text-xs text-muted-foreground">{guide.estimatedMinutes}min</span>
{/if}
<span class="text-xs text-muted-foreground">{steps.length} Schritte</span>
{#if completedRuns.length > 0}
<span class="text-xs text-muted-foreground">{completedRuns.length}× abgeschlossen</span>
{/if}
</div>
</div>
</div>
<div class="flex gap-2">
<button
onclick={() => (showEditModal = true)}
class="rounded-lg p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
aria-label="Bearbeiten"
>✏️</button>
{#if editMode}
<div class="flex flex-shrink-0 gap-1">
<button
onclick={() => (showGuideEditModal = true)}
class="rounded-lg p-2 text-sm text-muted-foreground hover:bg-surface"
title="Guide-Einstellungen"
>✏️</button>
<button
onclick={() => (showDeleteConfirm = true)}
class="rounded-lg p-2 text-sm text-muted-foreground hover:bg-red-50 hover:text-red-600"
title="Anleitung löschen"
>🗑</button>
</div>
{/if}
</div>
</div>
<!-- Active run banner -->
{#if activeRun}
{#if activeRun && !editMode}
<div class="mb-6 rounded-xl border border-primary/30 bg-primary/5 p-4">
<div class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium text-primary">Aktiver Durchlauf</span>
<span class="text-xs text-muted-foreground">{getActiveRunProgress()}% abgeschlossen</span>
<span class="text-xs text-muted-foreground">{getActiveRunProgress()}%</span>
</div>
<div class="mb-3 h-1.5 overflow-hidden rounded-full bg-primary/20">
<div
class="h-full rounded-full bg-primary transition-all"
style="width: {getActiveRunProgress()}%"
></div>
<div class="h-full rounded-full bg-primary transition-all" style="width: {getActiveRunProgress()}%"></div>
</div>
<button
onclick={continueRun}
class="w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white hover:bg-primary-hover"
>
<button onclick={continueRun} class="w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-white hover:bg-primary-hover">
Fortsetzen →
</button>
</div>
{:else}
<!-- Start run buttons -->
{:else if !editMode}
<div class="mb-8 flex gap-3">
<button
onclick={() => startRun('scroll')}
class="flex-1 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-white hover:bg-primary-hover"
disabled={steps.length === 0}
class="flex-1 rounded-xl bg-primary px-4 py-3 text-sm font-semibold text-white hover:bg-primary-hover disabled:opacity-40"
>
▶ Durchlauf starten
</button>
<button
onclick={() => startRun('focus')}
class="rounded-xl border border-border px-4 py-3 text-sm font-medium text-foreground hover:bg-accent"
disabled={steps.length === 0}
class="rounded-xl border border-border px-4 py-3 text-sm font-medium text-foreground hover:bg-accent disabled:opacity-40"
title="Fokus-Modus: ein Schritt auf einmal"
>
🎯 Fokus
@ -168,81 +261,157 @@
</div>
{/if}
<!-- Steps -->
<!-- Steps + Edit controls -->
<div class="space-y-6">
<!-- Sections with steps -->
{#if sections.length > 0}
{#each sections as section (section.id)}
{@const sectionSteps = getStepsForSection(section.id)}
<div>
<h2 class="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{section.title}
</h2>
<div class="mb-2 flex items-center gap-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{section.title}
</h2>
{#if editMode}
<button
onclick={() => deleteSection(section.id)}
class="ml-auto text-xs text-red-400 hover:text-red-600"
title="Abschnitt löschen"
>✕</button>
{/if}
</div>
<div class="space-y-2">
{#each getStepsForSection(section.id) as step (step.id)}
{#each sectionSteps as step, i (step.id)}
{@const cfg = stepTypeConfig[step.type]}
<div class="rounded-lg border-l-2 bg-surface px-4 py-3 {cfg.color}">
<div class="flex items-start gap-2">
<span class="mt-0.5 text-xs font-mono text-muted-foreground">{cfg.icon}</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-foreground">{step.title}</p>
{#if step.content}
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap">{step.content}</p>
{/if}
</div>
<div class="group flex items-start gap-2 rounded-xl border-l-2 px-4 py-3 {cfg.border} {cfg.bg} bg-surface">
<span class="mt-0.5 w-5 flex-shrink-0 text-center text-xs font-mono text-muted-foreground">{cfg.icon}</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-foreground">{step.title}</p>
{#if step.content}
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap line-clamp-2">{step.content}</p>
{/if}
</div>
{#if editMode}
<div class="flex flex-shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button onclick={() => moveStep(step, 'up')} disabled={i === 0} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20"></button>
<button onclick={() => moveStep(step, 'down')} disabled={i === sectionSteps.length - 1} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20"></button>
<button onclick={() => openEditStep(step)} class="p-1 text-muted-foreground hover:text-foreground"></button>
<button onclick={() => deleteStep(step.id)} class="p-1 text-red-400 hover:text-red-600"></button>
</div>
{/if}
</div>
{/each}
{#if editMode}
<button
onclick={() => openAddStep(section.id)}
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
>
<span class="text-lg leading-none">+</span>
Schritt hinzufügen
</button>
{/if}
</div>
</div>
{/each}
{/if}
{#if getUnsectionedSteps().length > 0}
<div class="space-y-2">
{#each getUnsectionedSteps() as step (step.id)}
{@const cfg = stepTypeConfig[step.type]}
<div class="rounded-lg border-l-2 bg-surface px-4 py-3 {cfg.color}">
<p class="text-sm font-medium text-foreground">{step.title}</p>
</div>
{/each}
</div>
{/if}
{:else}
<!-- Unsectioned steps -->
{#if getUnsectionedSteps().length > 0 || (sections.length === 0)}
{@const unsectioned = getUnsectionedSteps()}
<div class="space-y-2">
{#each steps as step (step.id)}
{#each unsectioned as step, i (step.id)}
{@const cfg = stepTypeConfig[step.type]}
<div class="rounded-lg border-l-2 bg-surface px-4 py-3 {cfg.color}">
<div class="flex items-start gap-2">
<span class="mt-0.5 text-xs font-mono text-muted-foreground">{cfg.icon}</span>
<div class="flex-1">
<p class="text-sm font-medium text-foreground">{step.title}</p>
{#if step.content}
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap">{step.content}</p>
{/if}
</div>
<div class="group flex items-start gap-2 rounded-xl border-l-2 px-4 py-3 {cfg.border} {cfg.bg} bg-surface">
<span class="mt-0.5 w-5 flex-shrink-0 text-center text-xs font-mono text-muted-foreground">{cfg.icon}</span>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-foreground">{step.title}</p>
{#if step.content}
<p class="mt-1 text-xs text-muted-foreground whitespace-pre-wrap line-clamp-2">{step.content}</p>
{/if}
</div>
{#if editMode}
<div class="flex flex-shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<button onclick={() => moveStep(step, 'up')} disabled={i === 0} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20"></button>
<button onclick={() => moveStep(step, 'down')} disabled={i === unsectioned.length - 1} class="p-1 text-muted-foreground hover:text-foreground disabled:opacity-20"></button>
<button onclick={() => openEditStep(step)} class="p-1 text-muted-foreground hover:text-foreground"></button>
<button onclick={() => deleteStep(step.id)} class="p-1 text-red-400 hover:text-red-600"></button>
</div>
{/if}
</div>
{/each}
{#if editMode}
<button
onclick={() => openAddStep()}
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
>
<span class="text-lg leading-none">+</span>
Schritt hinzufügen
</button>
{/if}
</div>
{/if}
<!-- Empty state -->
{#if steps.length === 0 && !editMode}
<div class="rounded-xl border border-dashed border-border py-12 text-center">
<p class="mb-3 text-sm text-muted-foreground">Noch keine Schritte</p>
<button
onclick={() => (editMode = true)}
class="text-sm font-medium text-primary hover:underline"
>
Bearbeiten um Schritte hinzuzufügen →
</button>
</div>
{/if}
<!-- Edit mode: add section / add step (top-level) -->
{#if editMode}
<div class="mt-2 space-y-2 border-t border-border pt-4">
{#if showAddSection}
<div class="flex gap-2">
<input
type="text"
bind:value={newSectionTitle}
placeholder="Abschnitt-Titel"
autofocus
onkeydown={(e) => { if (e.key === 'Enter') addSection(); if (e.key === 'Escape') { showAddSection = false; newSectionTitle = ''; } }}
class="flex-1 rounded-xl border border-border bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<button onclick={addSection} class="rounded-xl bg-primary px-4 py-2 text-sm text-white hover:bg-primary-hover">OK</button>
<button onclick={() => { showAddSection = false; newSectionTitle = ''; }} class="rounded-xl border border-border px-3 py-2 text-sm text-muted-foreground"></button>
</div>
{:else}
<button
onclick={() => (showAddSection = true)}
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground transition-colors hover:border-primary/40 hover:text-primary"
>
<span class="text-lg leading-none"></span>
Abschnitt hinzufügen
</button>
{/if}
</div>
{/if}
</div>
<!-- Run history -->
{#if completedRuns.length > 0}
<div class="mt-10">
{#if completedRuns.length > 0 && !editMode}
<div class="mt-10 border-t border-border pt-6">
<h2 class="mb-3 text-sm font-semibold text-foreground">Verlauf</h2>
<div class="space-y-2">
{#each completedRuns.slice(0, 5) as run (run.id)}
{@const doneCount = Object.values(run.stepStates).filter((s) => s.done).length}
<div class="flex items-center justify-between rounded-lg bg-surface px-4 py-2.5 text-sm">
<div class="flex items-center gap-2">
<span class="text-green-500"></span>
<span class="text-foreground">
{new Date(run.startedAt).toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric'
})}
{new Date(run.startedAt).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}
</span>
</div>
<span class="text-xs text-muted-foreground">
{Object.values(run.stepStates).filter((s) => s.done).length}/{totalSteps} Schritte
</span>
<span class="text-xs text-muted-foreground">{doneCount}/{checkableSteps.length} Schritte · {run.mode === 'focus' ? '🎯' : '📜'}</span>
</div>
{/each}
</div>
@ -250,3 +419,54 @@
{/if}
</div>
{/if}
<!-- Modals -->
{#if showGuideEditModal && guide}
<GuideEditModal
open={true}
{guide}
onClose={() => (showGuideEditModal = false)}
onSave={async (data) => {
await guidesStore.updateGuide(guide!.id, data);
showGuideEditModal = false;
}}
onDelete={async (id) => {
showGuideEditModal = false;
await guidesStore.deleteGuide(id);
goto('/');
}}
/>
{/if}
{#if stepModalOpen}
<StepEditorModal
open={true}
step={editingStep}
{guideId}
sectionId={stepModalSectionId}
order={0}
onClose={() => (stepModalOpen = false)}
onSave={handleSaveStep}
/>
{/if}
<!-- Delete confirm -->
{#if showDeleteConfirm}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" onmousedown={(e) => e.target === e.currentTarget && (showDeleteConfirm = false)}>
<div class="w-full max-w-sm rounded-2xl bg-background p-6 shadow-xl">
<h3 class="mb-2 text-lg font-semibold text-foreground">Anleitung löschen?</h3>
<p class="mb-6 text-sm text-muted-foreground">
<strong>{guide?.title}"</strong> und alle Schritte werden unwiderruflich gelöscht.
</p>
<div class="flex gap-3">
<button onclick={() => (showDeleteConfirm = false)} class="flex-1 rounded-xl border border-border py-2.5 text-sm text-foreground hover:bg-accent">
Abbrechen
</button>
<button onclick={handleDeleteGuide} class="flex-1 rounded-xl bg-red-500 py-2.5 text-sm font-medium text-white hover:bg-red-600">
Löschen
</button>
</div>
</div>
</div>
{/if}