diff --git a/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte b/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte
index cc4f941..90d5e40 100644
--- a/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte
+++ b/src/lib/components/blocks/StellingnahmeGeneratorBlock.svelte
@@ -89,18 +89,74 @@
// Quick-filter for allgemeine arguments (matches title / text / bulletPoints)
let argFilter = $state("");
+ // Tag-Pill-Filter (OR-Logik): wenn ≥1 Tag aktiv, matchen nur Fragmente
+ // mit mindestens einem aktiven Tag. Kombiniert mit argFilter via AND.
+ let activeTags = $state>(new Set());
+
+ function tagLabel(slug: string, name?: string): string {
+ if (name) return name;
+ return slug
+ .replace(/^tag-?/, "")
+ .replace(/-/g, " ")
+ .replace(/^\w/, (c) => c.toUpperCase());
+ }
+
+ function fragmentTagSlugs(f: TextFragment): string[] {
+ return (f.tags ?? [])
+ .map((t) => (typeof t === "string" ? t : t._slug))
+ .filter((s): s is string => !!s && s !== fetchTagSlug);
+ }
+
+ const availableTags = $derived.by((): { slug: string; label: string }[] => {
+ const map = new Map();
+ for (const f of allgemeineDeduped) {
+ for (const t of f.tags ?? []) {
+ const slug = typeof t === "string" ? t : t._slug;
+ if (!slug || slug === fetchTagSlug) continue;
+ if (!map.has(slug)) {
+ const name = typeof t === "string" ? undefined : t.name;
+ map.set(slug, tagLabel(slug, name));
+ }
+ }
+ }
+ return Array.from(map.entries())
+ .map(([slug, label]) => ({ slug, label }))
+ .sort((a, b) => a.label.localeCompare(b.label, "de"));
+ });
+
+ function toggleTag(slug: string) {
+ const next = new Set(activeTags);
+ if (next.has(slug)) next.delete(slug);
+ else next.add(slug);
+ activeTags = next;
+ }
+
+ function clearTagFilter() {
+ activeTags = new Set();
+ }
+
const filteredGruppen = $derived.by((): Group[] => {
const q = argFilter.trim().toLowerCase();
- if (!q) return allgemeineGruppen;
+ const tagFilterActive = activeTags.size > 0;
+ if (!q && !tagFilterActive) return allgemeineGruppen;
return allgemeineGruppen
.map((g) => ({
...g,
items: g.items.filter((f) => {
- const hay = [f.title, f.text, ...(f.bulletPoints ?? [])]
- .filter(Boolean)
- .join(" ")
- .toLowerCase();
- return hay.includes(q);
+ // Text-Match
+ if (q) {
+ const hay = [f.title, f.text, ...(f.bulletPoints ?? [])]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
+ if (!hay.includes(q)) return false;
+ }
+ // Tag-Match (OR über aktive Tags)
+ if (tagFilterActive) {
+ const slugs = fragmentTagSlugs(f);
+ if (!slugs.some((s) => activeTags.has(s))) return false;
+ }
+ return true;
}),
}))
.filter((g) => g.items.length > 0);
@@ -115,6 +171,21 @@
let historyExpanded = $state(false);
let mounted = $state(false);
let hadSavedState = $state(false);
+ // Auto-Save-Indikator: idle → saving (kurz) → saved (2s sichtbar) → idle.
+ let saveStatus = $state<"idle" | "saving" | "saved">("idle");
+ let savedHideTimer: ReturnType | null = null;
+ function flashSaved() {
+ saveStatus = "saved";
+ if (savedHideTimer) clearTimeout(savedHideTimer);
+ savedHideTimer = setTimeout(() => {
+ saveStatus = "idle";
+ }, 2000);
+ }
+
+ // Svelte-Action: fokussiert das Element beim Mount (Step-Wechsel).
+ function autofocus(node: HTMLElement) {
+ setTimeout(() => node.focus({ preventScroll: true }), 0);
+ }
function saveState() {
try {
@@ -133,7 +204,10 @@
$effect(() => {
void [selectedSlugs, persoenlicheEingabe, absenderName, absenderAnschrift, absenderOrt, argNotes, openingOverride, closingOverride];
- if (mounted) saveState();
+ if (mounted) {
+ saveState();
+ flashSaved();
+ }
});
// ── Step state ───────────────────────────────────────────────────────────────
@@ -142,7 +216,6 @@
let step = $state(0);
let selectedSlugs = $state>(new Set());
- let expandedGroups = $state>(new Set()); // empty = all collapsed
let expandedItems = $state>(new Set()); // per-item text expand
function toggleItem(slug: string) {
@@ -201,13 +274,6 @@
expandedItems = nextExpanded;
}
- function toggleGroup(slug: string) {
- const next = new Set(expandedGroups);
- if (next.has(slug)) next.delete(slug);
- else next.add(slug);
- expandedGroups = next;
- }
-
function toggleGroupSelection(group: Group) {
const slugs = group.items.map((f) => f._slug);
const allSelected = slugs.every((s) => selectedSlugs.has(s));
@@ -217,11 +283,6 @@
else next.add(s);
}
selectedSlugs = next;
- // Expand on select, collapse on deselect
- const nextGroups = new Set(expandedGroups);
- if (allSelected) nextGroups.delete(group.slug);
- else nextGroups.add(group.slug);
- expandedGroups = nextGroups;
}
// Combined selection for output — ortskonkrete first, then allgemeine
@@ -325,7 +386,6 @@ ${"=".repeat(64)}`;
}
selectedSlugs = new Set();
expandedItems = new Set();
- expandedGroups = new Set();
argNotes = {};
openingOverride = null;
closingOverride = null;
@@ -551,7 +611,15 @@ ${"=".repeat(64)}`;
{/if}
{/each}
- Schritt {step + 1} von {TOTAL_STEPS} – {STEP_LABELS[step]}
+
+ {#if saveStatus === "saved"}
+
+
+ Gespeichert
+
+ {/if}
+ Schritt {step + 1} von {TOTAL_STEPS} – {STEP_LABELS[step]}
+
{#if step === 0}
@@ -666,7 +734,7 @@ ${"=".repeat(64)}`;
2. Entwurf des Regionalplans Südwestthüringen zu verfassen.
Sie wählen zuerst ortskonkrete Argumente für Ihr Gebiet, dann allgemeine Einwände.
-
+
{/if}
-
+
goTo(0)}>← Zurück
goTo(2)}>
Weiter{ortskonkreteSelectedCount > 0 ? ` (${ortskonkreteSelectedCount} gewählt)` : ""} →
@@ -767,30 +835,50 @@ ${"=".repeat(64)}`;
placeholder="Argumente durchsuchen …"
bind:value={argFilter}
aria-label="Argumente durchsuchen"
+ use:autofocus
/>
+ {#if availableTags.length > 1}
+
+ {#each availableTags as tag}
+ {@const active = activeTags.has(tag.slug)}
+ toggleTag(tag.slug)}
+ aria-pressed={active}
+ class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors
+ {active
+ ? 'border-wald-700 bg-wald-700 text-white'
+ : 'border-stein-300 bg-white text-stein-600 hover:border-wald-400 hover:text-wald-700'}"
+ >{tag.label}
+ {/each}
+ {#if activeTags.size > 0}
+ Filter zurücksetzen
+ {/if}
+
+ {/if}
{#if filteredGruppen.length === 0}
-
Keine Argumente passen zu „{argFilter}".
+
+ Keine Argumente {argFilter ? `passen zu „${argFilter}"` : ""}{argFilter && activeTags.size > 0 ? " und " : ""}{activeTags.size > 0 ? "matchen die gewählten Themen" : ""}.
+
{/if}
{#each filteredGruppen as group}
{@const groupSelectedCount = group.items.filter((f) => selectedSlugs.has(f._slug)).length}
{@const allSelected = groupSelectedCount === group.items.length}
- {@const isExpanded = expandedGroups.has(group.slug) || argFilter.trim() !== ""}
-
+
- toggleGroup(group.slug)}
- aria-expanded={isExpanded}
- >
-
- {group.label}
-
+
+ {group.label}
+
{groupSelectedCount > 0 ? `${groupSelectedCount}/` : ""}{group.items.length}
-
+
-
- {#if isExpanded}
-
+
{#each group.items as f}
@@ -844,7 +930,6 @@ ${"=".repeat(64)}`;
{/each}
- {/if}
{/each}
@@ -854,7 +939,7 @@ ${"=".repeat(64)}`;
{/if}
-
+
goTo(1)}>← Zurück
Bleibt auf Ihrem Gerät. Wird nicht übertragen.
-
+
goTo(2)}>← Zurück
goTo(4)}>Weiter →
@@ -899,7 +985,7 @@ ${"=".repeat(64)}`;
Vor- und Nachname
-
+
Straße und Hausnummer
@@ -954,7 +1040,7 @@ ${"=".repeat(64)}`;
-
+
goTo(3)}>← Zurück
"
},
+ "home-group": {
+ "body": "
"
+ },
+ "vector-square": {
+ "body": "
"
+ },
+ "wind-turbine": {
+ "body": "
"
+ },
+ "arrow-expand-vertical": {
+ "body": "
"
+ },
+ "domain": {
+ "body": "
"
+ },
+ "flag-outline": {
+ "body": "
"
+ },
"transmission-tower": {
"body": "
"
},
@@ -220,9 +238,6 @@
"tag-outline": {
"body": "
"
},
- "flag-outline": {
- "body": "
"
- },
"information-outline": {
"body": "
"
}