feat(guides): collection selector in guide modal, quick inline step add

GuideEditModal:
- Live collection list loaded from IndexedDB
- Select dropdown to assign guide to a collection (or none)
- Collection persisted on save

Guide detail — quick step add (no modal needed):
- Click "+ Schritt hinzufügen" → inline input appears
- Enter to save, Esc to cancel, ⋯ to open full StepEditorModal
- Works in both sections and unsectioned step list
- Input stays open after adding (for rapid multi-step entry)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 21:26:07 +02:00
parent 1a999f8cca
commit 7f1c83f8b6
2 changed files with 96 additions and 16 deletions

View file

@ -1,5 +1,7 @@
<script lang="ts">
import type { LocalGuide, Difficulty } from '$lib/data/local-store.js';
import { liveQuery } from 'dexie';
import type { LocalGuide, Difficulty, LocalCollection } from '$lib/data/local-store.js';
import { collectionCollection } from '$lib/data/local-store.js';
import type { BaseRecord } from '@manacore/local-store';
type GuideInput = Omit<LocalGuide, keyof BaseRecord>;
@ -22,8 +24,18 @@
let difficulty = $state<Difficulty>(guide?.difficulty ?? 'easy');
let estimatedMinutes = $state(guide?.estimatedMinutes ?? 0);
let tagsInput = $state((guide?.tags ?? []).join(', '));
let collectionId = $state<string | undefined>(guide?.collectionId);
let saving = $state(false);
// Live collections for selector
let allCollections = $state<LocalCollection[]>([]);
$effect(() => {
const sub = liveQuery(() => collectionCollection.getAll()).subscribe((cols) => {
allCollections = cols;
});
return () => sub.unsubscribe();
});
// Re-init when guide prop changes
$effect(() => {
if (guide) {
@ -34,6 +46,7 @@
difficulty = guide.difficulty;
estimatedMinutes = guide.estimatedMinutes ?? 0;
tagsInput = guide.tags.join(', ');
collectionId = guide.collectionId;
}
});
@ -68,7 +81,7 @@
difficulty,
estimatedMinutes: estimatedMinutes > 0 ? estimatedMinutes : undefined,
tags,
collectionId: guide?.collectionId,
collectionId: collectionId || undefined,
orderInCollection: guide?.orderInCollection,
xpReward: guide?.xpReward,
skillId: guide?.skillId,
@ -217,6 +230,22 @@
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>
<!-- Collection -->
{#if allCollections.length > 0}
<div>
<label class="mb-1 block text-xs font-medium text-muted-foreground">Sammlung</label>
<select
bind:value={collectionId}
class="w-full rounded-xl border border-border bg-surface px-3 py-2.5 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/30"
>
<option value={undefined}>Keine Sammlung</option>
{#each allCollections as col}
<option value={col.id}>{col.coverEmoji ?? ''} {col.title}</option>
{/each}
</select>
</div>
{/if}
</div>
<!-- Footer -->

View file

@ -77,6 +77,25 @@
let editingStep = $state<LocalStep | undefined>(undefined);
let stepModalSectionId = $state<string | undefined>(undefined);
// Quick-add state (inline, no modal)
let quickAddSectionId = $state<string | 'root' | null>(null);
let quickAddTitle = $state('');
async function quickAddStep(sectionId?: string) {
if (!quickAddTitle.trim()) { quickAddSectionId = null; return; }
const targetSteps = sectionId ? getStepsForSection(sectionId) : getUnsectionedSteps();
await guidesStore.createStep({
guideId,
sectionId,
order: targetSteps.length,
title: quickAddTitle.trim(),
type: 'instruction',
checkable: true,
});
quickAddTitle = '';
// keep open for next step
}
function openAddStep(sectionId?: string) {
editingStep = undefined;
stepModalSectionId = sectionId;
@ -305,13 +324,26 @@
{/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 quickAddSectionId === section.id}
<div class="flex gap-2">
<input type="text" bind:value={quickAddTitle}
placeholder="Schritt-Titel (Enter = hinzufügen)" autofocus
onkeydown={async (e) => {
if (e.key === 'Enter') await quickAddStep(section.id);
if (e.key === 'Escape') { quickAddSectionId = null; quickAddTitle = ''; }
}}
class="flex-1 rounded-xl border border-primary/50 bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<button onclick={() => quickAddStep(section.id)} class="rounded-xl bg-primary px-3 py-2 text-sm text-white">+</button>
<button onclick={() => { quickAddSectionId = null; quickAddTitle = ''; }} class="rounded-xl border border-border px-3 py-2 text-sm text-muted-foreground"></button>
<button onclick={() => openAddStep(section.id)} class="rounded-xl border border-border px-2 py-2 text-xs text-muted-foreground hover:bg-accent" title="Erweitert"></button>
</div>
{:else}
<button onclick={() => { quickAddSectionId = section.id; quickAddTitle = ''; }}
class="flex w-full items-center gap-2 rounded-xl border border-dashed border-border px-4 py-3 text-sm text-muted-foreground hover:border-primary/40 hover:text-primary">
<span class="text-lg leading-none">+</span> Schritt hinzufügen
</button>
{/if}
{/if}
</div>
</div>
@ -344,13 +376,32 @@
{/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 quickAddSectionId === 'root'}
<div class="flex gap-2">
<input
type="text"
bind:value={quickAddTitle}
placeholder="Schritt-Titel (Enter = hinzufügen)"
autofocus
onkeydown={async (e) => {
if (e.key === 'Enter') await quickAddStep(undefined);
if (e.key === 'Escape') { quickAddSectionId = null; quickAddTitle = ''; }
}}
class="flex-1 rounded-xl border border-primary/50 bg-surface px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<button onclick={() => quickAddStep(undefined)} class="rounded-xl bg-primary px-3 py-2 text-sm text-white hover:bg-primary-hover">+</button>
<button onclick={() => { quickAddSectionId = null; quickAddTitle = ''; }} class="rounded-xl border border-border px-3 py-2 text-sm text-muted-foreground"></button>
<button onclick={() => openAddStep(undefined)} class="rounded-xl border border-border px-3 py-2 text-xs text-muted-foreground hover:bg-accent" title="Erweitert bearbeiten"></button>
</div>
{:else}
<button
onclick={() => { quickAddSectionId = 'root'; quickAddTitle = ''; }}
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}
{/if}
</div>
{/if}