refactor: Stellingnahme-Generator UX-Überarbeitung Step 5
Deploy / verify (push) Successful in 1m4s
Deploy / deploy (push) Successful in 1m15s

- Sticky farb-hinterlegter Stepper (Sprung zwischen Schritten klarer)
- Step 5 reorganisiert: Setup oben, "═══ Ergebnis ═══" Trenner,
  Schreibvorlage- und KI-Cards primär sichtbar, Feintuning
  (Vorschau, Argument-Reihenfolge, Einleitung, Abschluss) in
  ein collapsed "Brief im Detail anpassen"-Accordion zusammengelegt
- "Mit Platzhaltern füllen"-Button für Absender-Felder
- KI-Block: nummerierte 1-2-3-4-Anleitung, "In Zwischenablage
  kopieren"-Button + .txt-Download, editierbare Inhalts-Regeln
- KI-Prompt nachgeschärft: konkrete Rechtsgrundlagen, klare Anti-
  KI-Floskel-Liste, expliziter Hinweis "Entwurf-Banner entfernen",
  Verbot von em/en-dashes als Satz-Trenner
- buildText post-processed em/en-dashes zu Kommas
- UI-Texte: em/en-dashes durch Kommas ersetzt
- "Anlagen geplant"-Disclaimer (Schätzung ~4-6 WEA/100ha) in
  Stellingnahme-Card und WindAreaPanel ergänzt
- Reset setzt Step + Scroll zurück, History bleibt erhalten

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-06-07 11:02:48 +02:00
parent ace3562111
commit 6d1a084973
4 changed files with 471 additions and 214 deletions
+4
View File
@@ -69,6 +69,10 @@ const EXTRA_ICONS = [
'cursor-default-click-outline',
// CalendarBlock: social-image modal button
'image-outline',
// StellingnahmeGenerator: KI-Regeln-Accordion + Detail-Anpassen
'tune-variant',
'tune',
'check-decagram',
];
const STATIC_PATTERN = /icon="mdi:([a-z0-9-]+)"/g;
+15 -3
View File
@@ -126,9 +126,15 @@
</div>
{/if}
{#if area.anlagen_geplant != null}
<div class="flex items-center justify-between px-3 py-1.5">
<span class="text-xs text-stein-500">{t(T.windkarte_label_anlagen_geplant)}</span>
<span class="font-semibold text-stein-800">{area.anlagen_geplant}</span>
<div
class="flex items-center justify-between px-3 py-1.5"
title="Schätzung nach Faustregel ~46 Anlagen je 100 ha. Keine amtliche Festlegung — tatsächliche Anlagenzahl wird erst im Genehmigungsverfahren bestimmt."
>
<span class="flex items-center gap-1 text-xs text-stein-500">
{t(T.windkarte_label_anlagen_geplant)}
<Icon icon="mdi:information-outline" class="size-3 text-stein-400" />
</span>
<span class="font-semibold text-stein-800">~{area.anlagen_geplant}<span class="ml-0.5 text-xs text-stein-400">*</span></span>
</div>
{/if}
{#if area.hoehe_max_m != null}
@@ -144,6 +150,12 @@
</div>
{/if}
</div>
{#if area.anlagen_geplant != null}
<p class="-mt-2 px-1 text-[10px] leading-snug text-stein-400">
<span class="font-semibold">* Anlagenzahl:</span>
Schätzung (~46 Anlagen je 100 ha) — keine amtliche Festlegung.
</p>
{/if}
<!-- Gemeinden -->
{#if area.gemeinden && area.gemeinden.length > 0}
@@ -36,7 +36,7 @@
(block.allgemeineArgumente ?? []) as TextFragment[],
);
// Tag used to fetch allgemeineArgumente exclude from grouping categories
// Tag used to fetch allgemeineArgumente, exclude from grouping categories
const fetchTagSlug = $derived(
typeof block.allgemeineArgumenteTag === "object" && block.allgemeineArgumenteTag !== null
? (block.allgemeineArgumenteTag as { _slug: string })._slug
@@ -251,11 +251,11 @@
const gebietsBez = $derived(
windArea
? `${windArea.gebiets_nr}${windArea.bezeichnung ? " " + windArea.bezeichnung : ""}`
? `${windArea.gebiets_nr}${windArea.bezeichnung ? ", " + windArea.bezeichnung : ""}`
: "",
);
// Editable opening/closing null = use reactive default
// Editable opening/closing, null = use reactive default
const defaultOpening = $derived(
block.defaultOpeningText
? block.defaultOpeningText.replace("{gebiet}", gebietsBez)
@@ -309,7 +309,7 @@
];
});
// Combined selection for output respects drag order
// Combined selection for output, respects drag order
const selectedFragments = $derived.by(() => {
const allFrags = [...kriterien, ...allgemeineDeduped];
return fragmentOrder
@@ -332,7 +332,7 @@ Karl-Liebknecht-Straße 4
const mailtoLink = $derived.by(() => {
if (!outputText) return "";
const subject = `Einwendung Regionalplan Südwestthüringen Vorranggebiet ${gebietsBez}`;
const subject = `Einwendung Regionalplan Südwestthüringen, Vorranggebiet ${gebietsBez}`;
const to = block.recipientEmail ?? "";
return `mailto:${to}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(outputText)}`;
});
@@ -349,7 +349,7 @@ Karl-Liebknecht-Straße 4
? `Mit freundlichen Grüßen\n\n${absenderName.trim()}`
: "Mit freundlichen Grüßen";
const disclaimer = `${"=".repeat(64)}
!! ENTWURF KEIN FERTIGER BRIEF !!
!! ENTWURF, KEIN FERTIGER BRIEF !!
Stichpunkte müssen vor dem Einreichen in eigene, ausformulierte
Sätze überführt werden. Text individuell anpassen.
${"=".repeat(64)}`;
@@ -371,7 +371,11 @@ ${"=".repeat(64)}`;
return [disclaimer, "", sender, "", heute, "", "An:", RECIPIENT, "", "Betreff: " + betreff.split("\n")[0], betreff.split("\n")[1] ?? "", "", "Sehr geehrte Damen und Herren,", "", body.trimEnd()]
.join("\n")
.replace(/\n{3,}/g, "\n\n");
.replace(/\n{3,}/g, "\n\n")
// Em- und En-Dashes als Satz-Trenner durch Kommas ersetzen
// ("Wort, Wort" / "Wort, Wort" → "Wort, Wort").
// Bindestriche in Bezeichnungen wie "W-23" bleiben unangetastet.
.replace(/\s+[—–]\s+/g, ", ");
}
let sectionEl: HTMLElement | undefined;
@@ -398,6 +402,25 @@ ${"=".repeat(64)}`;
const aiToneLabels = ["sehr sachlich & rational", "eher sachlich", "ausgewogen", "eher emotional", "sehr emotional & persönlich betroffen"];
const aiSharpnessLabels = ["konstruktiv & kooperativ", "moderat", "bestimmt & klar", "kritisch", "fordernd & scharf"];
// Editierbare Inhalts-Regeln für den KI-Prompt. Default-Set unten,
// Nutzer kann anpassen/ergänzen, wirkt als Override im Prompt.
const DEFAULT_AI_RULES = `1. Alle Argumente, Fakten, Zahlen, Gesetzes- und Quellenverweise aus der Vorlage MÜSSEN vollständig erhalten bleiben. Reihenfolge im Hauptteil darfst du variieren, Inhalt nicht.
2. KEINE neuen Fakten, Zahlen, Gesetzesnormen, Studien oder Quellen erfinden. Was nicht in der Vorlage steht, kommt nicht in den Output. (Der oben angegebene Kontext zum Vorranggebiet und die persönliche Betroffenheit zählen ebenfalls als Vorlage.)
3. Stichpunkte, Bullet-Listen oder Aufzählungen aus der Vorlage werden in zusammenhängende Fließtext-Absätze überführt. Im fertigen Brief stehen keine "•"/"-"-Listen mehr im Argumentationsteil.
4. Mindestens 70 % des Fließtextes sind neu formuliert. Übernimm keine ganzen Sätze 1:1 aus der Vorlage. Behalte juristische Fachbegriffe und Zitate bei.
5. Wenn der Verfasser persönliche Betroffenheit angegeben hat, wird sie konkret und ich-bezogen in einen passenden Absatz des Hauptteils eingewoben, nicht als separater Block angehängt. Ohne Angabe verzichtest du auf erfundene persönliche Bezüge.`;
let aiRules = $state(DEFAULT_AI_RULES);
// Badges für "Brief im Detail anpassen"-Accordion
const tweakBadges = $derived(
[
selectedFragments.length > 1 ? "Reihenfolge" : null,
openingOverride !== null ? "Einleitung" : null,
closingOverride !== null ? "Abschluss" : null,
hasManualEdit ? "bearbeitet" : null,
].filter(Boolean) as string[],
);
const stepFilled = $derived([
!!windArea,
ortskonkreteSelectedCount > 0 || kriterien.length === 0,
@@ -439,8 +462,15 @@ ${"=".repeat(64)}`;
absenderOrt = "";
showOutput = false;
outputText = "";
hasManualEdit = false;
fragmentOrder = [];
hadSavedState = false;
maxStepReached = 0;
step = 0;
try { localStorage.removeItem(storageKey); } catch {}
if (typeof window !== "undefined") {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
let dragIdx = $state<number | null>(null);
@@ -480,7 +510,7 @@ ${"=".repeat(64)}`;
}
}
async function copyAiPrompt() {
function buildAiPrompt(): string {
const toneDesc = aiToneLabels[aiTone - 1];
const sharpnessDesc = aiSharpnessLabels[aiSharpness - 1];
const customPart = aiCustomNote.trim()
@@ -490,42 +520,70 @@ ${"=".repeat(64)}`;
? `\nPersönliche Betroffenheit des Verfassers (zentral und authentisch einbinden, möglichst wörtlich verarbeiten, nichts dazuerfinden):\n${persoenlicheEingabe.trim()}\n`
: "";
const gebietPart = windArea
? `\nKontext zum betroffenen Windvorranggebiet: ${windArea.gebiets_nr ?? ""}${windArea.bezeichnung ? " " + windArea.bezeichnung : ""}${windArea.gemeinden?.length ? " (Gemeinden: " + windArea.gemeinden.slice(0, 5).join(", ") + ")" : ""}\n`
? `\nKontext zum betroffenen Windvorranggebiet: ${windArea.gebiets_nr ?? ""}${windArea.bezeichnung ? ", " + windArea.bezeichnung : ""}${windArea.gemeinden?.length ? " (Gemeinden: " + windArea.gemeinden.slice(0, 5).join(", ") + ")" : ""}\n`
: "";
const prompt = `Rolle: Du bist ein erfahrener Verfasser von Bürger-Einwendungen im Raumordnungs- und Genehmigungsverfahren. Du formulierst eine individuelle, juristisch und sachlich saubere Stellungnahme im Namen des Verfassers.
return `# Rolle
Du bist erfahrener Verfasser von Bürger-Einwendungen im Raumordnungs- und Genehmigungsverfahren nach BauGB, ROG und BImSchG. Du kennst die Sprache und Konventionen behördlicher Anschreiben in Deutschland und schreibst rechtlich tragfähig, sachlich, ohne Pathos.
Aufgabe: Schreibe die unten als "MUSTER-STELLUNGNAHME" beigefügte Vorlage so um, dass sie wie eine eigenständig verfasste Einwendung dieser einen Person wirkt nicht wie eine Vorlage und nicht wie eine generierte Massenstellungnahme.
# Aufgabe
Aus der unten als "MUSTER-STELLUNGNAHME" beigefügten Vorlage (= Argument-Gerüst mit Stichpunkten) entsteht ein vollständig ausformulierter Einwendungstext, der wie eine eigenständig verfasste Einwendung dieser konkreten Person wirkt, nicht wie eine Vorlage, nicht wie eine generierte Massenstellungnahme, nicht wie ein offensichtlich KI-erzeugter Text.
Stilvorgaben:
# Stilvorgaben
- Ton: ${toneDesc}
- Haltung/Schärfe: ${sharpnessDesc}
- Sprache: durchgängig Deutsch, Sie-Form gegenüber der Behörde, klar adressiert.
- Sprache: Deutsch, durchgängig Sie-Form gegenüber der Behörde. Sätze klar, Hauptsatz-dominiert, ohne Verschachtelungen über drei Ebenen.
- Verwende juristisch-sachliche Begriffe ("erheben Einwendung", "Abwägungsfehler", "ersatzlose Streichung"), aber keine Floskeln und keine Übertreibungen.
${gebietPart}${personalPart}${customPart}
Zwingende Regeln:
1. ALLE inhaltlichen Argumente, Fakten, Zahlen, Gesetzes- und Quellenverweise aus der Muster-Stellungnahme MÜSSEN vollständig erhalten bleiben. Nichts weglassen, nichts inhaltlich abschwächen.
2. KEINE neuen Fakten, Zahlen, Gesetzesnormen oder Quellen erfinden. Wenn etwas in der Vorlage nicht steht, taucht es im Output nicht auf.
3. Briefkopf, Behörden-Adresse, Aktenzeichen/Betreff, Datum, Anrede und Schlussformel inkl. Absender bleiben strukturell erhalten (Reihenfolge, Felder, Angaben). Nur Formulierungen im Fließtext darfst du umarbeiten.
4. Argument-Reihenfolge im Hauptteil darfst du variieren, sofern Logik und Lesefluss stimmen.
5. Formulierungen variieren keine Satzbausteine 1:1 übernehmen. Mindestens 70 % des Fließtextes neu formulieren, ohne Inhalt zu verändern.
6. Persönliche Betroffenheit (falls oben angegeben) wird konkret und ich-bezogen in den Text eingewoben, nicht als separater Absatz angehängt.
7. Länge: ungefähr im Rahmen der Vorlage (±20 %). Keine Aufblähung mit Floskeln, keine starke Kürzung.
# Inhaltliche Regeln (zwingend)
${aiRules.trim()}
Ausgabeformat:
- Gib AUSSCHLIESSLICH den fertigen Stellungnahme-Text aus. Kein Vorwort, keine Erklärung, keine Markdown-Codeblöcke, keine Hinweise wie "Hier ist deine Stellungnahme:". Direkt mit dem Briefkopf beginnen.
# Brief-Struktur (unveränderlich)
- Absender (Name, Anschrift, PLZ/Ort), Datum, Behörden-Adresse, Betreff und Schlussformel werden 1:1 aus der Vorlage übernommen, Reihenfolge, Felder, exakte Schreibweise. Auch dann, wenn ein Feld leer oder kurz wirkt (z.B. nur ein Vorname).
- Anrede und Grußformel ("Sehr geehrte Damen und Herren," / "Mit freundlichen Grüßen") bleiben unverändert.
# Was du im Output ENTFERNST
- Alle Hinweis-Banner, Warnhinweise, Platzhalter oder Anweisungen für den Verfasser ("ENTWURF, KEIN FERTIGER BRIEF", "Stichpunkte ausformulieren", Trennstriche wie "====" oder "---"). Diese gehören nicht in den endgültigen Behördentext.
- Meta-Kommentare, Erklärungen, Markdown-Headlines (## …), Codeblöcke (\`\`\`).
# Länge & Qualität
- Länge ungefähr im Rahmen der Vorlage (±25 %). Keine Aufblähung mit Floskeln ("vor diesem Hintergrund möchte ich betonen, dass …"), keine starke Kürzung argumentativer Substanz.
- Vermeide KI-typische Muster: "Es ist wichtig zu beachten, dass …", "darüber hinaus möchte ich anmerken …", "in einer Welt, in der …", übermäßige Adverbien ("zudem", "weiterhin", "des Weiteren" in jedem Absatz).
# Zeichensetzung
- KEINE Gedankenstriche (em-dash "—" oder en-dash "") als Satz-Trenner verwenden. Stattdessen Kommas, Klammern oder Doppelpunkte nutzen. Bindestriche in Bezeichnungen ("W-23", "Karl-Liebknecht-Straße") bleiben.
- Bevorzuge typografisch deutsche Anführungszeichen („…") wo passend.
# Ausgabeformat
Gib AUSSCHLIESSLICH den fertigen Brieftext aus, beginnend mit dem Absenderblock, endend mit Namen unter der Grußformel. Kein Vorwort, keine Erklärung, keine Markdown-Codeblöcke, keine Schlussbemerkung wie "Bei Fragen stehe ich gerne zur Verfügung" (außer es steht so in der Vorlage).
MUSTER-STELLUNGNAHME:
---
${outputText}
---`;
}
async function copyAiPrompt() {
const prompt = buildAiPrompt();
try {
await navigator.clipboard.writeText(prompt);
copyAiPromptFeedback = "Kopiert!";
setTimeout(() => (copyAiPromptFeedback = ""), 2000);
copyAiPromptFeedback = "✓ In Zwischenablage kopiert";
setTimeout(() => (copyAiPromptFeedback = ""), 2500);
} catch {
copyAiPromptFeedback = "Fehler";
}
}
function downloadAiPrompt() {
const prompt = buildAiPrompt();
const blob = new Blob([prompt], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `ki-prompt-${windArea?.gebiets_nr ?? "stellungnahme"}.txt`;
a.click();
URL.revokeObjectURL(url);
}
function printText() {
const w = window.open("", "_blank");
if (!w) return;
@@ -658,9 +716,9 @@ ${outputText}
<Icon icon="mdi:clock-alert-outline" class="size-5 shrink-0" />
<span>
Einwendungsfrist: <strong>{deadlineFormatted}</strong>
{#if deadlineDays === 0} <strong>heute letzter Tag!</strong>
{:else if deadlineDays === 1} <strong>morgen letzter Tag!</strong>
{:else} noch {deadlineDays} Tage
{#if deadlineDays === 0}, <strong>heute letzter Tag!</strong>
{:else if deadlineDays === 1}, <strong>morgen letzter Tag!</strong>
{:else}, noch {deadlineDays} Tage
{/if}
</span>
</div>
@@ -701,48 +759,50 @@ ${outputText}
<div class="prose prose-wald mb-6 max-w-none text-stein-700">{@html md(block.intro)}</div>
{/if}
<!-- Step indicator -->
<div class="mb-2 flex items-center gap-1 text-xs font-medium">
{#each Array.from({ length: TOTAL_STEPS }, (_, i) => i) as s}
<button
class="flex h-7 w-7 items-center justify-center rounded-full border transition-colors
{s === step
? 'border-wald-700 bg-wald-700 text-white'
: s <= maxStepReached && stepFilled[s]
? 'border-wald-500 bg-wald-100 text-wald-700'
: s <= maxStepReached
? 'border-erde-400 bg-erde-50 text-erde-600'
: 'border-stein-300 bg-white text-stein-400'}"
onclick={() => { if (s <= maxStepReached) goTo(s as Step); }}
disabled={s > maxStepReached}
aria-current={s === step ? "step" : undefined}
title={STEP_LABELS[s]}
>
{#if s !== step && s <= maxStepReached && stepFilled[s]}
<Icon icon="mdi:check" class="size-3.5" />
{:else}
{s + 1}
<!-- Step indicator (sticky) -->
<div class="stellingnahme-stepper sticky top-2 z-[1100] -mx-3 mb-6 rounded-xl border border-wald-200 bg-wald-50/95 px-4 py-3 shadow-md backdrop-blur sm:mx-0">
<div class="flex items-center gap-1 text-xs font-medium">
{#each Array.from({ length: TOTAL_STEPS }, (_, i) => i) as s}
<button
class="flex h-8 w-8 items-center justify-center rounded-full border-2 font-semibold transition-all
{s === step
? 'border-wald-700 bg-wald-700 text-white shadow-md scale-110'
: s <= maxStepReached && stepFilled[s]
? 'border-wald-500 bg-wald-200 text-wald-800 hover:bg-wald-300'
: s <= maxStepReached
? 'border-erde-400 bg-erde-50 text-erde-700 hover:bg-erde-100'
: 'border-stein-300 bg-white text-stein-400'}"
onclick={() => { if (s <= maxStepReached) goTo(s as Step); }}
disabled={s > maxStepReached}
aria-current={s === step ? "step" : undefined}
title={STEP_LABELS[s]}
>
{#if s !== step && s <= maxStepReached && stepFilled[s]}
<Icon icon="mdi:check" class="size-4" />
{:else}
{s + 1}
{/if}
</button>
{#if s < TOTAL_STEPS - 1}
<div class="h-0.5 flex-1 rounded {s < maxStepReached ? (stepFilled[s] ? 'bg-wald-400' : 'bg-erde-300') : 'bg-stein-200'}"></div>
{/if}
</button>
{#if s < TOTAL_STEPS - 1}
<div class="h-px flex-1 {s < maxStepReached ? (stepFilled[s] ? 'bg-wald-400' : 'bg-erde-300') : 'bg-stein-200'}"></div>
{/each}
</div>
<div class="mt-2 flex items-center justify-between gap-3 text-xs">
<span class="font-semibold text-wald-800">Schritt {step + 1} / {TOTAL_STEPS}, {STEP_LABELS[step]}</span>
{#if saveStatus === "saved"}
<span class="flex items-center gap-1 text-wald-600" aria-live="polite">
<Icon icon="mdi:check-circle" class="size-3.5" aria-hidden="true" />
Gespeichert
</span>
{/if}
{/each}
</div>
<div class="mb-6 flex items-center justify-end gap-3 text-xs">
{#if saveStatus === "saved"}
<span class="flex items-center gap-1 text-wald-600 transition-opacity" aria-live="polite">
<Icon icon="mdi:check-circle" class="size-3.5" aria-hidden="true" />
Gespeichert
</span>
{/if}
<span class="text-stein-400">Schritt {step + 1} von {TOTAL_STEPS} {STEP_LABELS[step]}</span>
</div>
</div>
<!-- ── Step 0: Gebietskontext ─────────────────────────────────────────────── -->
{#if step === 0}
<div>
<h3 class="mb-4 text-lg font-semibold text-wald-800">Schritt 1 Ihr Vorranggebiet</h3>
<h3 class="mb-4 text-lg font-semibold text-wald-800">Schritt 1, Ihr Vorranggebiet</h3>
{#if windArea}
<div class="mb-6 rounded-xl border border-wald-200 bg-wald-50 p-5">
@@ -769,8 +829,16 @@ ${outputText}
{#if windArea.anlagen_geplant}
<div class="rounded-lg border border-wald-200 bg-white/70 p-3 shadow-sm">
<Icon icon="mdi:wind-turbine" class="mb-1 size-5 text-wald-600" aria-hidden="true" />
<dt class="whitespace-nowrap text-sm font-semibold text-wald-800 sm:text-base">Anlagen geplant</dt>
<dd class="mt-1 text-stein-600">{windArea.anlagen_geplant}</dd>
<dt class="flex items-center gap-1 whitespace-nowrap text-sm font-semibold text-wald-800 sm:text-base">
Anlagen geplant
<span
class="cursor-help text-stein-400 hover:text-stein-600"
title="Schätzung nach Faustregel ~46 Anlagen je 100 ha. Keine amtliche Festlegung, tatsächliche Anlagenzahl wird erst im Genehmigungsverfahren bestimmt."
>
<Icon icon="mdi:information-outline" class="size-3.5" />
</span>
</dt>
<dd class="mt-1 text-stein-600">~{windArea.anlagen_geplant}<span class="ml-0.5 text-xs text-stein-400">*</span></dd>
</div>
{/if}
{#if windArea.hoehe_max_m}
@@ -795,6 +863,15 @@ ${outputText}
</div>
{/if}
</dl>
{#if windArea.anlagen_geplant}
<p class="mt-3 text-[11px] leading-snug text-stein-500">
<span class="font-semibold">* Hinweis zur Anlagenzahl:</span>
keine amtliche Festlegung, eigene Schätzung nach Faustregel
~46 Anlagen je 100 ha. Aus dem Prüfbogen ZU-14 stammen nur
Flächengröße und Windhöffigkeit. Die tatsächliche Anlagenzahl
wird erst im Genehmigungsverfahren bestimmt.
</p>
{/if}
{#if windArea.notizen}
<details class="mt-4 border-t border-wald-200 pt-3" open>
<summary class="flex cursor-pointer items-center gap-1 text-sm font-medium text-wald-700 list-none">
@@ -867,7 +944,7 @@ ${outputText}
<!-- ── Step 1: Ortskonkrete Argumente ───────────────────────────────────── -->
{:else if step === 1}
<div>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 2 Ortskonkrete Argumente</h3>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 2, Ortskonkrete Argumente</h3>
<p class="mb-5 text-sm text-stein-600">
Diese Argumente beziehen sich direkt auf das Gebiet <strong>{windArea?.gebiets_nr ?? ""}</strong>.
Wählen Sie die Punkte, die auf Ihre Situation zutreffen.
@@ -936,7 +1013,7 @@ ${outputText}
<!-- ── Step 2: Allgemeine Argumente ─────────────────────────────────────── -->
{:else if step === 2}
<div>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 3 Grundsätzliche Einwände</h3>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 3, Grundsätzliche Einwände</h3>
<p class="mb-1 text-sm text-stein-600">
Welche allgemeinen Argumente teilen Sie? Diese gelten unabhängig vom konkreten Standort.
</p>
@@ -1076,7 +1153,7 @@ ${outputText}
<!-- ── Step 3: Persönliche Betroffenheit ─────────────────────────────────── -->
{:else if step === 3}
<div>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 4 Persönliche Betroffenheit</h3>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 4, Persönliche Betroffenheit</h3>
<p class="mb-2 text-sm text-stein-600">
Beschreiben Sie kurz, wie Sie persönlich betroffen sind.
<strong>Wichtig:</strong> Individuelle Betroffenheit stärkt Ihre Einwendung erheblich
@@ -1106,8 +1183,25 @@ ${outputText}
<!-- ── Step 4: Absender + Ausgabe ────────────────────────────────────────── -->
{:else if step === 4}
<div>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 5 Absender & Ausgabe</h3>
<p class="mb-4 text-sm text-stein-600">Ihre Daten werden nur lokal zur Texterstellung genutzt.</p>
<h3 class="mb-1 text-lg font-semibold text-wald-800">Schritt 5, Absender & Ausgabe</h3>
<p class="mb-3 text-sm text-stein-600">Ihre Daten werden nur lokal zur Texterstellung genutzt.</p>
<div class="mb-3 flex flex-wrap items-center gap-2">
<button
type="button"
class="inline-flex items-center gap-1.5 rounded-md border border-stein-300 bg-white px-2.5 py-1.5 text-[11px] font-medium text-stein-700 hover:bg-stein-50"
onclick={() => {
absenderName = "[Vor- und Nachname]";
absenderAnschrift = "[Straße und Hausnummer]";
absenderOrt = "[PLZ und Ort]";
}}
title="Felder mit Platzhaltern füllen, später im fertigen Text in eckigen Klammern suchen und ersetzen."
>
<Icon icon="mdi:format-text-variant-outline" class="size-3.5 text-stein-500" />
Mit Platzhaltern füllen
</button>
<span class="text-[11px] text-stein-400">Erzeugt z.B. <code class="rounded bg-stein-100 px-1">[Vor- und Nachname]</code>, später ersetzen.</span>
</div>
<div class="mb-5 grid gap-3 sm:grid-cols-2">
<div>
@@ -1124,160 +1218,289 @@ ${outputText}
</div>
</div>
{#if selectedFragments.length > 1}
<details class="mb-5 rounded-lg border border-stein-200 overflow-hidden">
<summary class="flex cursor-pointer items-center justify-between bg-stein-50 px-4 py-2.5 text-sm font-medium text-stein-700 hover:bg-stein-100 list-none">
<span>Reihenfolge der Argumente ({selectedFragments.length})</span>
<Icon icon="mdi:drag-vertical" class="size-4 text-stein-400" />
</summary>
<div class="divide-y divide-stein-100">
{#each selectedFragments as frag, i}
<div
draggable="true"
ondragstart={() => onDragStart(i)}
ondragover={(e) => onDragOver(e, i)}
ondrop={() => onDrop(i)}
ondragend={onDragEnd}
class="flex cursor-grab items-center gap-3 px-4 py-2.5 text-sm transition-colors active:cursor-grabbing
{dragOverIdx === i && dragIdx !== i ? 'bg-wald-50 border-t-2 border-wald-400' : 'bg-white hover:bg-stein-50'}"
>
<Icon icon="mdi:drag-vertical" class="size-4 shrink-0 text-stein-400" />
<span class="flex-1 truncate text-stein-700">{frag.title ?? frag._slug.replace(/_/g, " ")}</span>
<span class="shrink-0 text-[10px] text-stein-400">{i + 1}</span>
</div>
{/each}
</div>
</details>
{/if}
<div class="mb-5 rounded-lg border border-stein-200 bg-stein-50 p-3 text-xs text-stein-600">
<div class="mb-1 font-semibold text-stein-700">Einzureichen bei:</div>
<div class="whitespace-pre">{RECIPIENT}</div>
</div>
<!-- Einleitung & Abschluss anpassen -->
<div class="mb-4 space-y-2">
<details class="rounded-lg border border-stein-200 overflow-hidden">
<summary class="flex cursor-pointer items-center justify-between bg-stein-50 px-4 py-2.5 text-sm font-medium text-stein-700 hover:bg-stein-100 list-none">
<span>Einleitung anpassen</span>
<Icon icon="mdi:chevron-down" class="det-chevron size-4 text-stein-400" />
</summary>
<div class="border-t border-stein-100 px-4 py-3">
<textarea
class="w-full rounded border border-stein-200 px-3 py-2 text-sm text-stein-800 focus:border-wald-400 focus:outline-none focus:ring-1 focus:ring-wald-200 resize-y"
rows="3"
value={effectiveOpening}
oninput={(e) => (openingOverride = (e.target as HTMLTextAreaElement).value)}
></textarea>
{#if openingOverride !== null}
<button class="mt-1 text-xs text-stein-400 hover:text-stein-600" onclick={() => (openingOverride = null)}>Zurücksetzen</button>
{/if}
</div>
</details>
<details class="rounded-lg border border-stein-200 overflow-hidden">
<summary class="flex cursor-pointer items-center justify-between bg-stein-50 px-4 py-2.5 text-sm font-medium text-stein-700 hover:bg-stein-100 list-none">
<span>Abschluss anpassen</span>
<Icon icon="mdi:chevron-down" class="det-chevron size-4 text-stein-400" />
</summary>
<div class="border-t border-stein-100 px-4 py-3">
<textarea
class="w-full rounded border border-stein-200 px-3 py-2 text-sm text-stein-800 focus:border-wald-400 focus:outline-none focus:ring-1 focus:ring-wald-200 resize-y"
rows="3"
value={effectiveClosing}
oninput={(e) => (closingOverride = (e.target as HTMLTextAreaElement).value)}
></textarea>
{#if closingOverride !== null}
<button class="mt-1 text-xs text-stein-400 hover:text-stein-600" onclick={() => (closingOverride = null)}>Zurücksetzen</button>
{/if}
</div>
</details>
</div>
<div class="sticky bottom-2 z-10 mb-2 flex gap-3 rounded-lg border border-stein-200 bg-white/95 p-3 shadow-lg backdrop-blur sm:static sm:border-0 sm:bg-transparent sm:p-0 sm:shadow-none sm:backdrop-blur-none">
<button class="rounded-lg border border-stein-300 px-4 py-2 text-sm text-stein-600 hover:bg-stein-50" onclick={() => goTo(3)}> Zurück</button>
<button
class="btn-wiggle rounded-lg bg-wald-700 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-wald-800 disabled:opacity-40"
disabled={!absenderName.trim()}
onclick={generateOutput}
title={!absenderName.trim() ? "Bitte erst Namen eintragen" : hasManualEdit ? "Manuelle Änderungen verwerfen und neu generieren" : undefined}
>{hasManualEdit ? "↺ Neu generieren" : "Text erstellen"}</button>
</div>
{#if !absenderName.trim()}
<p class="mb-4 text-xs text-erde-600">Bitte Namen eintragen, um den Text zu erstellen.</p>
{:else}
<div class="mb-4"></div>
<p class="mb-4 rounded-lg border border-erde-200 bg-erde-50 px-3 py-2 text-xs text-erde-700">
Bitte oben Ihren Namen eintragen, der Text wird automatisch erstellt.
</p>
{/if}
{#if showOutput}
<div class="rounded-xl border border-wald-300 bg-wald-50/30 shadow-sm" role="region" aria-live="polite" aria-label="Generierte Einwendung">
<div class="flex flex-col gap-2 border-b border-wald-200 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
<span class="text-sm font-semibold text-wald-800">Ihre Einwendung</span>
<div class="flex flex-wrap gap-2">
<button class="rounded-md border border-stein-300 px-2.5 py-1 text-[11px] font-medium text-stein-700 hover:bg-stein-50" onclick={copyText}>{copyFeedback || "Kopieren"}</button>
<button class="rounded-md border border-stein-300 px-2.5 py-1 text-[11px] font-medium text-stein-700 hover:bg-stein-50" onclick={downloadText}>Herunterladen</button>
<button class="rounded-md border border-stein-300 px-2.5 py-1 text-[11px] font-medium text-stein-700 hover:bg-stein-50" onclick={printText}>Drucken</button>
{#if mailtoLink && !mailTooLong}
<a href={mailtoLink} class="rounded-md border border-wald-400 bg-wald-50 px-2.5 py-1 text-[11px] font-medium text-wald-700 no-underline hover:bg-wald-100">{block.recipientEmail ? "Per E-Mail senden" : "Per E-Mail (an mich)"}</a>
{/if}
<!-- ═══ Trenner: Ergebnis ═══ -->
<div class="my-6 flex items-center gap-3">
<div class="h-px flex-1 bg-wald-200"></div>
<span class="flex items-center gap-1.5 rounded-full bg-wald-100 px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-wald-800">
<Icon icon="mdi:check-decagram" class="size-3.5" />
Ergebnis
</span>
<div class="h-px flex-1 bg-wald-200"></div>
</div>
<!-- Block 1: Schreibvorlage -->
<div class="mb-4 rounded-xl border border-wald-200 bg-white p-4 shadow-sm">
<div class="mb-2 flex items-start gap-2">
<Icon icon="mdi:file-document-edit-outline" class="mt-0.5 size-5 shrink-0 text-wald-700" />
<div class="flex-1">
<h4 class="text-sm font-semibold text-wald-800">Schreibvorlage verwenden</h4>
<p class="mt-0.5 text-xs text-stein-500">Text direkt übernehmen, kopieren, herunterladen oder drucken.</p>
</div>
</div>
<textarea
bind:value={outputText}
spellcheck="false"
oninput={() => { hasManualEdit = true; }}
class="w-full resize-none bg-transparent px-5 py-4 font-sans text-sm leading-relaxed text-stein-800 focus:outline-none"
style="min-height: 40rem; field-sizing: content;"
></textarea>
{#if mailTooLong}
<div class="border-t border-stein-100 px-5 py-2 text-xs text-stein-500">Für E-Mail zu lang — bitte <strong>Kopieren</strong> und in die Mail einfügen.</div>
{/if}
<div class="border-t border-himmel-100 bg-himmel-50/40 px-4 py-3">
<p class="mb-1 text-[11px] font-semibold uppercase tracking-wide text-himmel-700">Mit KI eine individuelle Version erstellen</p>
<p class="mb-3 text-[11px] text-stein-500">Stichpunkte in einen persönlichen Fließtext verwandeln — mit ChatGPT, Claude oder einer anderen KI:<br><span class="font-medium text-stein-600">① Regler einstellen &nbsp;② „KI-Prompt kopieren" klicken &nbsp;③ Bei ChatGPT / Claude einfügen &amp; senden</span></p>
<div class="mb-2 grid gap-3 sm:grid-cols-2">
<div>
<div class="mb-1.5">
<label for="ai-tone" class="text-[11px] text-stein-600">Ton: <span class="font-semibold text-himmel-700">{aiToneLabels[aiTone - 1]}</span></label>
</div>
<div class="flex items-center gap-2 overflow-visible py-1 px-2">
<span class="shrink-0 text-[10px] text-stein-400">Sachlich</span>
<input id="ai-tone" type="range" min="1" max="5" bind:value={aiTone} class="h-3 w-full cursor-pointer accent-himmel-500" />
<span class="shrink-0 text-[10px] text-stein-400">Emotional</span>
</div>
</div>
<div>
<div class="mb-1.5">
<label for="ai-sharpness" class="text-[11px] text-stein-600">Haltung: <span class="font-semibold text-himmel-700">{aiSharpnessLabels[aiSharpness - 1]}</span></label>
</div>
<div class="flex items-center gap-2 overflow-visible py-1 px-2">
<span class="shrink-0 text-[10px] text-stein-400">Konstruktiv</span>
<input id="ai-sharpness" type="range" min="1" max="5" bind:value={aiSharpness} class="h-3 w-full cursor-pointer accent-himmel-500" />
<span class="shrink-0 text-[10px] text-stein-400">Fordernd</span>
</div>
</div>
</div>
<div class="mb-2.5">
<label for="ai-custom-note" class="mb-1 block text-[11px] text-stein-600">Persönlicher Hinweis <span class="text-stein-400">(optional)</span></label>
<textarea
id="ai-custom-note"
bind:value={aiCustomNote}
rows="2"
placeholder='z.B. „Ich bin Landwirt und direkt betroffen“ oder „Bitte sehr kurz halten“'
class="w-full resize-none rounded border border-himmel-200 bg-white px-2.5 py-1.5 text-[11px] text-stein-700 placeholder:text-stein-300 focus:border-himmel-400 focus:outline-none"
></textarea>
</div>
<div class="flex flex-wrap gap-2">
<button
class="rounded-md border border-himmel-400 bg-himmel-100 px-3 py-1.5 text-[11px] font-medium text-himmel-800 hover:bg-himmel-200"
onclick={copyAiPrompt}
>{copyAiPromptFeedback || "KI-Prompt kopieren"}</button>
class="inline-flex items-center gap-1.5 rounded-md border border-wald-300 bg-wald-50 px-3 py-1.5 text-xs font-medium text-wald-800 hover:bg-wald-100"
onclick={copyText}
>
<Icon icon="mdi:content-copy" class="size-3.5" />
{copyFeedback || "Kopieren"}
</button>
<button
class="inline-flex items-center gap-1.5 rounded-md border border-stein-300 bg-white px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50"
onclick={downloadText}
>
<Icon icon="mdi:download" class="size-3.5" />
Herunterladen
</button>
<button
class="inline-flex items-center gap-1.5 rounded-md border border-stein-300 bg-white px-3 py-1.5 text-xs font-medium text-stein-700 hover:bg-stein-50"
onclick={printText}
>
<Icon icon="mdi:printer" class="size-3.5" />
Drucken
</button>
{#if mailtoLink && !mailTooLong}
<a
href={mailtoLink}
class="inline-flex items-center gap-1.5 rounded-md border border-wald-400 bg-wald-50 px-3 py-1.5 text-xs font-medium text-wald-700 no-underline hover:bg-wald-100"
>
<Icon icon="mdi:email-outline" class="size-3.5" />
{block.recipientEmail ? "Per E-Mail senden" : "Per E-Mail (an mich)"}
</a>
{/if}
</div>
</div>
<div class="mt-4 rounded-lg border border-himmel-200 bg-himmel-50 px-4 py-3 text-sm text-himmel-800">
<!-- Block 2: KI-Variante -->
<div class="mb-4 rounded-xl border border-himmel-200 bg-himmel-50/40 p-4 shadow-sm">
<div class="mb-3 flex items-start gap-2">
<Icon icon="mdi:robot-outline" class="mt-0.5 size-5 shrink-0 text-himmel-700" />
<div class="flex-1">
<h4 class="text-sm font-semibold text-himmel-800">Mit KI eine individuelle Version erstellen</h4>
<p class="mt-0.5 text-xs text-stein-500">
Die Vorlage wird zu persönlichem Fließtext umformuliert, mit ChatGPT, Claude oder anderer KI.
</p>
</div>
</div>
<ol class="mb-3 space-y-1.5 rounded-lg border border-himmel-100 bg-white/60 p-3 text-[11px] text-stein-700">
<li class="flex gap-2"><span class="font-semibold text-himmel-700">1.</span><span>Regler unten einstellen (Ton, Schärfe).</span></li>
<li class="flex gap-2"><span class="font-semibold text-himmel-700">2.</span><span>Auf <strong>„In Zwischenablage kopieren“</strong> klicken, der komplette Prompt wird automatisch kopiert. (Alternativ als <strong>.txt-Datei</strong> herunterladen.)</span></li>
<li class="flex gap-2"><span class="font-semibold text-himmel-700">3.</span><span>ChatGPT, Claude o.ä. öffnen → ins Eingabefeld <strong>einfügen</strong> (Strg+V / ⌘+V) → senden.</span></li>
<li class="flex gap-2"><span class="font-semibold text-himmel-700">4.</span><span>KI-Antwort prüfen, ggf. anpassen, fertig.</span></li>
</ol>
<div class="mb-3 grid gap-3 sm:grid-cols-2">
<div>
<div class="mb-1.5">
<label for="ai-tone" class="text-[11px] text-stein-600">Ton: <span class="font-semibold text-himmel-700">{aiToneLabels[aiTone - 1]}</span></label>
</div>
<div class="flex items-center gap-2 overflow-visible py-1 px-2">
<span class="shrink-0 text-[10px] text-stein-400">Sachlich</span>
<input id="ai-tone" type="range" min="1" max="5" bind:value={aiTone} class="h-3 w-full cursor-pointer accent-himmel-500" />
<span class="shrink-0 text-[10px] text-stein-400">Emotional</span>
</div>
</div>
<div>
<div class="mb-1.5">
<label for="ai-sharpness" class="text-[11px] text-stein-600">Haltung: <span class="font-semibold text-himmel-700">{aiSharpnessLabels[aiSharpness - 1]}</span></label>
</div>
<div class="flex items-center gap-2 overflow-visible py-1 px-2">
<span class="shrink-0 text-[10px] text-stein-400">Konstruktiv</span>
<input id="ai-sharpness" type="range" min="1" max="5" bind:value={aiSharpness} class="h-3 w-full cursor-pointer accent-himmel-500" />
<span class="shrink-0 text-[10px] text-stein-400">Fordernd</span>
</div>
</div>
</div>
<div class="mb-3">
<label for="ai-custom-note" class="mb-1 block text-[11px] text-stein-600">Persönlicher Hinweis <span class="text-stein-400">(optional)</span></label>
<textarea
id="ai-custom-note"
bind:value={aiCustomNote}
rows="2"
placeholder='z.B. „Ich bin Landwirt und direkt betroffen“ oder „Bitte sehr kurz halten“'
class="w-full resize-none rounded border border-himmel-200 bg-white px-2.5 py-1.5 text-[11px] text-stein-700 placeholder:text-stein-300 focus:border-himmel-400 focus:outline-none"
></textarea>
</div>
<details class="mb-3 rounded-lg border border-himmel-200 bg-white/70 overflow-hidden">
<summary class="flex cursor-pointer items-center justify-between bg-himmel-50 px-3 py-2 text-[11px] font-medium text-himmel-800 hover:bg-himmel-100 list-none">
<span class="flex items-center gap-1.5">
<Icon icon="mdi:tune-variant" class="size-3.5" />
Inhaltliche Regeln für die KI feinjustieren
{#if aiRules.trim() !== DEFAULT_AI_RULES.trim()}
<span class="rounded-full bg-himmel-200 px-1.5 py-0.5 text-[9px] text-himmel-800">angepasst</span>
{/if}
</span>
<Icon icon="mdi:chevron-down" class="det-chevron size-4 text-himmel-600" />
</summary>
<div class="border-t border-himmel-100 p-3">
<p class="mb-2 text-[10px] text-stein-500">
Diese Regeln werden 1:1 in den KI-Prompt übernommen. Anpassen, wenn die KI z.B. zu locker mit Fakten umgeht oder zu sehr abweicht. Bei Unsicherheit → zurücksetzen.
</p>
<textarea
bind:value={aiRules}
rows="8"
spellcheck="false"
class="w-full resize-y rounded border border-himmel-200 bg-white px-2.5 py-1.5 font-mono text-[11px] leading-snug text-stein-700 focus:border-himmel-400 focus:outline-none"
></textarea>
<div class="mt-1.5 flex items-center justify-between">
<span class="text-[10px] text-stein-400">{aiRules.length} Zeichen</span>
<button
type="button"
class="text-[10px] text-stein-500 underline-offset-2 hover:text-stein-700 hover:underline"
onclick={() => (aiRules = DEFAULT_AI_RULES)}
disabled={aiRules.trim() === DEFAULT_AI_RULES.trim()}
>Auf Standard zurücksetzen</button>
</div>
</div>
</details>
<div class="flex flex-wrap gap-2">
<button
class="inline-flex items-center gap-1.5 rounded-md border border-himmel-500 bg-himmel-600 px-3.5 py-2 text-xs font-semibold text-white shadow-sm hover:bg-himmel-700"
onclick={copyAiPrompt}
>
<Icon icon="mdi:content-copy" class="size-3.5" />
{copyAiPromptFeedback || "In Zwischenablage kopieren"}
</button>
<button
class="inline-flex items-center gap-1.5 rounded-md border border-himmel-300 bg-white px-3.5 py-2 text-xs font-medium text-himmel-800 hover:bg-himmel-50"
onclick={downloadAiPrompt}
title="Prompt als Textdatei speichern, alternativ zur Zwischenablage."
>
<Icon icon="mdi:download" class="size-3.5" />
Als .txt herunterladen
</button>
</div>
</div>
<!-- Brief im Detail anpassen, alle Feintuning-Optionen kompakt -->
<details class="mb-4 rounded-xl border border-stein-200 bg-white overflow-hidden">
<summary class="flex cursor-pointer items-center justify-between bg-stein-50 px-4 py-3 text-sm font-medium text-stein-700 hover:bg-stein-100 list-none">
<span class="flex items-center gap-2">
<Icon icon="mdi:tune" class="size-4 text-stein-500" />
Brief im Detail anpassen
{#if tweakBadges.length > 0}
<span class="text-[10px] text-stein-400">({tweakBadges.join(" · ")})</span>
{/if}
</span>
<Icon icon="mdi:chevron-down" class="det-chevron size-4 text-stein-400" />
</summary>
<div class="border-t border-stein-200 p-4 space-y-4">
<!-- Vorschau -->
<div>
<p class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-stein-500">
<Icon icon="mdi:file-document-outline" class="size-3.5" />
Vorschau
{#if hasManualEdit}
<span class="rounded-full bg-himmel-100 px-2 py-0.5 text-[9px] font-normal text-himmel-700">bearbeitet</span>
{/if}
</p>
<textarea
bind:value={outputText}
spellcheck="false"
oninput={() => { hasManualEdit = true; }}
class="w-full resize-none rounded-lg border border-stein-200 bg-wald-50/30 px-4 py-3 font-sans text-sm leading-relaxed text-stein-800 focus:border-wald-400 focus:outline-none"
style="min-height: 20rem; field-sizing: content;"
></textarea>
{#if hasManualEdit}
<div class="mt-1 flex items-center justify-between text-xs">
<span class="text-stein-500">Manuelle Änderungen aktiv</span>
<button
class="rounded border border-stein-300 bg-white px-2 py-0.5 text-[11px] text-stein-600 hover:bg-stein-50"
onclick={generateOutput}
>↺ Neu aus Vorlage</button>
</div>
{/if}
{#if mailTooLong}
<p class="mt-1 text-xs text-stein-500">Für E-Mail zu lang, bitte <strong>Kopieren</strong> und in die Mail einfügen.</p>
{/if}
</div>
{#if selectedFragments.length > 1}
<div>
<p class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-stein-500">
<Icon icon="mdi:drag-vertical" class="size-3.5" />
Reihenfolge der Argumente ({selectedFragments.length})
</p>
<div class="divide-y divide-stein-100 rounded-lg border border-stein-200">
{#each selectedFragments as frag, i}
<div
draggable="true"
ondragstart={() => onDragStart(i)}
ondragover={(e) => onDragOver(e, i)}
ondrop={() => onDrop(i)}
ondragend={onDragEnd}
class="flex cursor-grab items-center gap-3 px-3 py-2 text-sm transition-colors active:cursor-grabbing
{dragOverIdx === i && dragIdx !== i ? 'bg-wald-50 border-t-2 border-wald-400' : 'bg-white hover:bg-stein-50'}"
>
<Icon icon="mdi:drag-vertical" class="size-4 shrink-0 text-stein-400" />
<span class="flex-1 truncate text-stein-700">{frag.title ?? frag._slug.replace(/_/g, " ")}</span>
<span class="shrink-0 text-[10px] text-stein-400">{i + 1}</span>
</div>
{/each}
</div>
</div>
{/if}
<div class="grid gap-3 sm:grid-cols-2">
<div>
<p class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-stein-500">
Einleitung
{#if openingOverride !== null}
<button class="text-[10px] text-stein-400 underline hover:text-stein-600" onclick={() => (openingOverride = null)}>zurücksetzen</button>
{/if}
</p>
<textarea
class="w-full resize-y rounded-lg border border-stein-200 px-3 py-2 text-sm text-stein-800 focus:border-wald-400 focus:outline-none focus:ring-1 focus:ring-wald-200"
rows="3"
value={effectiveOpening}
oninput={(e) => (openingOverride = (e.target as HTMLTextAreaElement).value)}
></textarea>
</div>
<div>
<p class="mb-1.5 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-stein-500">
Abschluss
{#if closingOverride !== null}
<button class="text-[10px] text-stein-400 underline hover:text-stein-600" onclick={() => (closingOverride = null)}>zurücksetzen</button>
{/if}
</p>
<textarea
class="w-full resize-y rounded-lg border border-stein-200 px-3 py-2 text-sm text-stein-800 focus:border-wald-400 focus:outline-none focus:ring-1 focus:ring-wald-200"
rows="3"
value={effectiveClosing}
oninput={(e) => (closingOverride = (e.target as HTMLTextAreaElement).value)}
></textarea>
</div>
</div>
</div>
</details>
<div class="mb-4 rounded-lg border border-himmel-200 bg-himmel-50 px-4 py-3 text-sm text-himmel-800">
<strong>Hinweis:</strong> Bitte passen Sie den Text vor dem Einreichen individuell an.
Einwendungen mit persönlichen Formulierungen sind wirksamer als identische Kopien.
</div>
{/if}
<!-- Bottom navigation -->
<div class="flex flex-wrap gap-3 border-t border-stein-200 pt-4">
<button class="rounded-lg border border-stein-300 px-4 py-2 text-sm text-stein-600 hover:bg-stein-50" onclick={() => goTo(3)}> Zurück</button>
<button class="rounded-lg border border-erde-300 px-4 py-2 text-sm text-erde-700 hover:bg-erde-50" onclick={resetAll}>
↻ Neustart
</button>
</div>
<!-- History -->
{#if outputHistory.length > 0}
<div class="mt-6">
+24 -6
View File
@@ -81,6 +81,9 @@
"download": {
"body": "<path fill=\"currentColor\" d=\"M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z\"/>"
},
"information-outline": {
"body": "<path fill=\"currentColor\" d=\"M11 9h2V7h-2m1 13c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m0-18A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m-1 15h2v-6h-2z\"/>"
},
"loading": {
"body": "<path fill=\"currentColor\" d=\"M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8\"/>"
},
@@ -177,6 +180,27 @@
"flag-outline": {
"body": "<path fill=\"currentColor\" d=\"m12.36 6l.4 2H18v6h-3.36l-.4-2H7V6zM14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6\"/>"
},
"format-text-variant-outline": {
"body": "<path fill=\"currentColor\" d=\"M11 3c-.82 0-1.56.5-1.86 1.27l-5.5 14C3.12 19.58 4.09 21 5.5 21h2.25c.84 0 1.58-.5 1.87-1.3l.64-1.7h3.48l.64 1.7c.29.8 1.04 1.3 1.87 1.3h2.25c1.41 0 2.38-1.42 1.86-2.73l-5.5-14C14.56 3.5 13.82 3 13 3m-2 2h2l5.5 14h-2.25l-1.13-3H8.87l-1.12 3H5.5M12 7.67L9.62 14h4.75Z\"/>"
},
"check-decagram": {
"body": "<path fill=\"currentColor\" d=\"m23 12l-2.44-2.78l.34-3.68l-3.61-.82l-1.89-3.18L12 3L8.6 1.54L6.71 4.72l-3.61.81l.34 3.68L1 12l2.44 2.78l-.34 3.69l3.61.82l1.89 3.18L12 21l3.4 1.46l1.89-3.18l3.61-.82l-.34-3.68zm-13 5l-4-4l1.41-1.41L10 14.17l6.59-6.59L18 9z\"/>"
},
"printer": {
"body": "<path fill=\"currentColor\" d=\"M18 3H6v4h12m1 5a1 1 0 0 1-1-1a1 1 0 0 1 1-1a1 1 0 0 1 1 1a1 1 0 0 1-1 1m-3 7H8v-5h8m3-6H5a3 3 0 0 0-3 3v6h4v4h12v-4h4v-6a3 3 0 0 0-3-3\"/>"
},
"robot-outline": {
"body": "<path fill=\"currentColor\" d=\"M17.5 15.5c0 1.11-.89 2-2 2s-2-.89-2-2s.9-2 2-2s2 .9 2 2m-9-2c-1.1 0-2 .9-2 2s.9 2 2 2s2-.89 2-2s-.89-2-2-2M23 15v3c0 .55-.45 1-1 1h-1v1c0 1.11-.89 2-2 2H5a2 2 0 0 1-2-2v-1H2c-.55 0-1-.45-1-1v-3c0-.55.45-1 1-1h1c0-3.87 3.13-7 7-7h1V5.73c-.6-.34-1-.99-1-1.73c0-1.1.9-2 2-2s2 .9 2 2c0 .74-.4 1.39-1 1.73V7h1c3.87 0 7 3.13 7 7h1c.55 0 1 .45 1 1m-2 1h-2v-2c0-2.76-2.24-5-5-5h-4c-2.76 0-5 2.24-5 5v2H3v1h2v3h14v-3h2z\"/>"
},
"tune-variant": {
"body": "<path fill=\"currentColor\" d=\"M8 13c-1.86 0-3.41 1.28-3.86 3H2v2h2.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22v-2H11.86c-.45-1.72-2-3-3.86-3m0 6c-1.1 0-2-.9-2-2s.9-2 2-2s2 .9 2 2s-.9 2-2 2M19.86 6c-.45-1.72-2-3-3.86-3s-3.41 1.28-3.86 3H2v2h10.14c.45 1.72 2 3 3.86 3s3.41-1.28 3.86-3H22V6zM16 9c-1.1 0-2-.9-2-2s.9-2 2-2s2 .9 2 2s-.9 2-2 2\"/>"
},
"tune": {
"body": "<path fill=\"currentColor\" d=\"M3 17v2h6v-2zM3 5v2h10V5zm10 16v-2h8v-2h-8v-2h-2v6zM7 9v2H3v2h4v2h2V9zm14 4v-2H11v2zm-6-4h2V7h4V5h-4V3h-2z\"/>"
},
"file-document-outline": {
"body": "<path fill=\"currentColor\" d=\"M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm0 2h7v5h5v11H6zm2 8v2h8v-2zm0 4v2h5v-2z\"/>"
},
"drag-vertical": {
"body": "<path fill=\"currentColor\" d=\"M9 3h2v2H9zm4 0h2v2h-2zM9 7h2v2H9zm4 0h2v2h-2zm-4 4h2v2H9zm4 0h2v2h-2zm-4 4h2v2H9zm4 0h2v2h-2zm-4 4h2v2H9zm4 0h2v2h-2z\"/>"
},
@@ -204,9 +228,6 @@
"home-outline": {
"body": "<path fill=\"currentColor\" d=\"m12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81zM12 3L2 12h3v8h6v-6h2v6h6v-8h3\"/>"
},
"file-document-outline": {
"body": "<path fill=\"currentColor\" d=\"M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm0 2h7v5h5v11H6zm2 8v2h8v-2zm0 4v2h5v-2z\"/>"
},
"share-variant-outline": {
"body": "<path fill=\"currentColor\" d=\"M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81c1.66 0 3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66c0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91s-1.31-2.92-2.92-2.92M18 4c.55 0 1 .45 1 1s-.45 1-1 1s-1-.45-1-1s.45-1 1-1M6 13c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1m12 7c-.55 0-1-.45-1-1s.45-1 1-1s1 .45 1 1s-.45 1-1 1\"/>"
},
@@ -267,9 +288,6 @@
},
"tag-outline": {
"body": "<path fill=\"currentColor\" d=\"m21.41 11.58l-9-9A2 2 0 0 0 11 2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 .59 1.42l9 9A2 2 0 0 0 13 22a2 2 0 0 0 1.41-.59l7-7A2 2 0 0 0 22 13a2 2 0 0 0-.59-1.42M13 20l-9-9V4h7l9 9M6.5 5A1.5 1.5 0 1 1 5 6.5A1.5 1.5 0 0 1 6.5 5\"/>"
},
"information-outline": {
"body": "<path fill=\"currentColor\" d=\"M11 9h2V7h-2m1 13c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m0-18A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m-1 15h2v-6h-2z\"/>"
}
},
"aliases": {