refactor(consolidate): IconButton + Badge-solid + @layer base + Massen-Migration
Deploy / verify (push) Failing after 1m21s
Deploy / deploy (push) Has been skipped

E: app.css Base-h1–h6 in @layer base → text-*!-Hacks in Titeln entfernt.
Neue Atoms: IconButton (runde Icon-Buttons), Badge solid-Variante + uppercase.

Migrationen (6 parallele Pässe):
- IconButton: 19 runde Nav/Close-Buttons (Carousel, Galleries, QuoteCarousel,
  PostOverview, Organisations, ImageModal).
- Badge: Kategorie/Status-Pills (PostCard, BlogOverview, kalender, Comments,
  CalendarBlock, FilesBlock, SearchableText, Stellingnahme).
- ui-Atome: Formular-Inputs (Comments, termin-melden, Stellingnahme) → TextInput/
  Textarea/Checkbox/SearchField; dunkle NewsletterInline bewusst raw.
- Button-Atom: ~20 inline Buttons in StellingnahmeGeneratorBlock.
- Radius-Sweep: Cards→rounded-lg (viele via .card-surface), Buttons/Inputs→rounded-md,
  Chips→rounded-full; StrommixBlock/Calendar/Files/Adressbuch u.a.

svelte-check 0 errors, voller Prod-Build grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-12 11:11:48 +02:00
parent fe7bce2935
commit 3679978d6c
29 changed files with 351 additions and 382 deletions
+7
View File
@@ -194,6 +194,10 @@ main {
Erlaubte Gewichte: 300, 400, 500, 600, 700. Inter primary, Lora nur Zitate.
========================================================================== */
/* @layer base: Element-Defaults liegen unter den Utilities → text-*/font-*-
Utilities auf h1h6 gewinnen ohne `!`-Hack (vorher unlayered = schlug alles). */
@layer base {
h1,
h2,
h3,
@@ -264,6 +268,9 @@ h4 {
}
}
}
/* /@layer base */
/* Page-Titel (wie www.windwiderstand.de) */
.pageTitle h1 {
font-size: 1.5rem;
+8 -1
View File
@@ -9,8 +9,10 @@
argTypes: {
color: {
control: 'select',
options: ['green', 'blue', 'amber', 'orange', 'purple', 'teal', 'slate', undefined],
options: ['green', 'blue', 'amber', 'orange', 'purple', 'teal', 'slate', 'fire', 'white', undefined],
},
variant: { control: 'select', options: ['subtle', 'solid'] },
uppercase: { control: 'boolean' },
},
});
</script>
@@ -24,3 +26,8 @@
<Story name="Purple" args={{ color: 'purple' }}>Ostthüringen</Story>
<Story name="Teal" args={{ color: 'teal' }}>Mittelthüringen</Story>
<Story name="Slate" args={{ color: 'slate' }}>Nordthüringen</Story>
<!-- Solid (gefüllt, weißer Text) — Kategorie/Status-Pills. -->
<Story name="Solid Green" args={{ variant: 'solid', color: 'green', uppercase: true }}>Neu</Story>
<Story name="Solid Slate" args={{ variant: 'solid', color: 'slate', uppercase: true }}>Vergangen</Story>
<Story name="Solid Fire" args={{ variant: 'solid', color: 'fire', uppercase: true }}>Live</Story>
+36 -5
View File
@@ -1,17 +1,28 @@
<script lang="ts">
import type { Snippet } from "svelte";
type Variant = "subtle" | "solid";
let {
children,
color,
variant = "subtle",
uppercase = false,
class: className = "",
}: { children?: Snippet; color?: string; class?: string } = $props();
}: {
children?: Snippet;
color?: string;
/** subtle = getönt mit Border (Default) · solid = gefüllt, weißer Text. */
variant?: Variant;
uppercase?: boolean;
class?: string;
} = $props();
// Die einzige Badge-Farbquelle. Deckt alle CMS-badge.color-Werte ab
// (green/blue/amber/orange/purple/teal/slate) + Fallback. Marken-Töne
// (wald/himmel) wo möglich, sonst Tailwind-Palette für klar unterscheidbare
// Regionen-Badges. KEINE Eigenimplementierungen woanders — immer <Badge>.
function colorClasses(c?: string): string {
// (wald/himmel) wo möglich, sonst Tailwind-Palette. KEINE Eigenimplementierungen
// woanders — immer <Badge>.
function subtleClasses(c?: string): string {
switch (c) {
case "green": return "bg-wald-50 text-wald-700 border-wald-200";
case "blue": return "bg-himmel-50 text-himmel-700 border-himmel-200";
@@ -23,10 +34,30 @@
default: return "bg-stein-100 text-stein-600 border-stein-200";
}
}
// Gefüllte Variante (weißer Text) — für Kategorie-/Status-Pills auf Bildern etc.
function solidClasses(c?: string): string {
switch (c) {
case "green": return "bg-wald-600 text-white border-transparent";
case "blue": return "bg-himmel-700 text-white border-transparent";
case "amber": return "bg-amber-500 text-white border-transparent";
case "orange": return "bg-orange-500 text-white border-transparent";
case "purple": return "bg-purple-600 text-white border-transparent";
case "teal": return "bg-teal-600 text-white border-transparent";
case "fire": return "bg-fire-700 text-white border-transparent";
case "white": return "bg-white/90 text-stein-800 border-transparent";
case "slate":
default: return "bg-stein-800 text-white border-transparent";
}
}
const colorClasses = $derived(
variant === "solid" ? solidClasses(color) : subtleClasses(color),
);
</script>
<span
class="shrink-0 whitespace-nowrap rounded-full border px-1.5 py-0 text-[0.65rem] font-medium {colorClasses(color)} {className}"
class="shrink-0 whitespace-nowrap rounded-full border px-1.5 py-0 text-[0.65rem] font-medium {uppercase
? 'uppercase tracking-wide'
: ''} {colorClasses} {className}"
>
{@render children?.()}
</span>
+9 -6
View File
@@ -330,7 +330,7 @@
</div>
{:else}
<div
class="rounded-xs border border-stein-200 bg-white p-6 text-center text-sm text-stein-600"
class="rounded-lg border border-stein-200 bg-white p-6 text-center text-sm text-stein-600"
>
{t(T.blog_no_results)}
</div>
@@ -448,7 +448,7 @@
{@const fp = featuredPost as PostWithImage}
<a
href={postHref(fp)}
class="not-prose group mb-6 grid overflow-hidden rounded-sm border border-stein-200 bg-white no-underline transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:border-wald-300 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500 focus-visible:ring-offset-2 md:grid-cols-2"
class="not-prose group mb-6 grid overflow-hidden rounded-lg border border-stein-200 bg-white no-underline transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:border-wald-300 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500 focus-visible:ring-offset-2 md:grid-cols-2"
>
<div class="relative aspect-video w-full overflow-hidden bg-stein-100 md:aspect-auto">
{#if fp._rawImageUrl}
@@ -468,12 +468,15 @@
class="h-full w-full object-cover"
/>
{/if}
<span
class="absolute left-3 top-3 inline-flex items-center gap-1 rounded-full bg-wald-600 px-2.5 py-1 text-[.65rem] font-semibold uppercase tracking-wide text-white shadow-sm"
<Badge
variant="solid"
color="green"
uppercase
class="absolute left-3 top-3 inline-flex items-center gap-1 shadow-sm"
>
<Icon icon="lucide:star" class="size-3" />
Neuester Beitrag
</span>
</Badge>
{#if featuredEvent?.eventDate}
<div class="absolute bottom-3 left-3 right-3 min-w-0">
<EventBadges
@@ -529,7 +532,7 @@
{#if currentPage === 1 && !activeTag}
<div
class="not-prose my-6 rounded-sm bg-stein-900 p-5 sm:p-6 [&_h3]:!text-stein-0"
class="not-prose my-6 rounded-lg bg-stein-900 p-5 sm:p-6 [&_h3]:!text-stein-0"
>
<NewsletterInlineFormComponent />
</div>
+3 -18
View File
@@ -1,8 +1,7 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import type { Snippet } from "svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import IconButton from "$lib/components/IconButton.svelte";
let {
count,
@@ -67,14 +66,7 @@
>
<div class="flex items-center gap-1.5 sm:gap-2">
{#if showArrows && count > 1}
<button
type="button"
onclick={prev}
aria-label="Vorheriger Eintrag"
class="shrink-0 inline-flex items-center justify-center size-7 rounded-full bg-white/90 border border-stein-200 shadow-sm hover:bg-white hover:border-wald-700 transition"
>
<Icon icon="lucide:chevron-left" class="size-4" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label="Vorheriger Eintrag" variant="surface" size="sm" onclick={prev} />
{/if}
<div class="grid flex-1 min-w-0" ontouchstart={onTouchStart} ontouchend={onTouchEnd}>
@@ -91,14 +83,7 @@
</div>
{#if showArrows && count > 1}
<button
type="button"
onclick={next}
aria-label="Nächster Eintrag"
class="shrink-0 inline-flex items-center justify-center size-7 rounded-full bg-white/90 border border-stein-200 shadow-sm hover:bg-white hover:border-wald-700 transition"
>
<Icon icon="lucide:chevron-right" class="size-4" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-right" label="Nächster Eintrag" variant="surface" size="sm" onclick={next} />
{/if}
</div>
+13 -13
View File
@@ -4,6 +4,8 @@
import { env } from "$env/dynamic/public";
import { useTranslate, T } from "$lib/translations";
import Button from "$lib/ui/Button.svelte";
import TextInput from "$lib/ui/TextInput.svelte";
import Badge from "$lib/components/Badge.svelte";
let {
pageId,
@@ -361,10 +363,10 @@
<span class="text-stein-400 text-xs" title={fmtAbs(c.updated_at)}>{t(T.comments_edited)}</span>
{/if}
{#if c.status === "pending"}
<span class="ml-auto inline-flex items-center gap-1 rounded-full bg-erde-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-erde-800">
<Badge color="amber" uppercase class="ml-auto inline-flex items-center gap-1">
<Icon icon="lucide:clock" class="size-3" />
{t(T.comments_pending_short)}
</span>
</Badge>
{/if}
</div>
{/snippet}
@@ -384,7 +386,7 @@
{/each}
</ul>
{:else if loadError}
<div class="flex items-start gap-2 rounded-xs border border-fire-200 bg-fire-50 p-3 text-sm text-fire-800">
<div class="flex items-start gap-2 rounded-lg border border-fire-200 bg-fire-50 p-3 text-sm text-fire-800">
<Icon icon="lucide:circle-alert" class="size-5 shrink-0 mt-0.5" />
<div class="flex-1">
<p class="font-medium">{t(T.comments_load_error_title)}</p>
@@ -408,7 +410,7 @@
{@const replies = repliesOf(c.id)}
<li
class="group flex gap-3 list-none! {c.status === 'pending'
? 'rounded-xs border border-erde-200 bg-erde-50/50 p-3'
? 'rounded-lg border border-erde-200 bg-erde-50/50 p-3'
: ''}"
>
{@render avatar(c.author)}
@@ -435,13 +437,12 @@
{#if replyTo === c.id}
<form onsubmit={(e) => onReply(e, c.id)} class="mt-3 space-y-2">
<input
type="text"
<TextInput
bind:value={replyAuthor}
placeholder={t(T.comments_name_placeholder)}
label=""
required
maxlength="80"
class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
size="sm"
/>
<textarea
bind:value={replyBody}
@@ -473,7 +474,7 @@
{#each replies as r (r.id)}
<li
class="group flex gap-3 list-none! {r.status === 'pending'
? 'rounded-xs border border-erde-200 bg-erde-50/50 p-2'
? 'rounded-lg border border-erde-200 bg-erde-50/50 p-2'
: ''}"
>
{@render avatar(r.author, "sm")}
@@ -493,13 +494,12 @@
<form onsubmit={onSubmit} class="border-t border-stein-200 pt-5 space-y-3">
<h3 class="font-medium text-sm text-stein-900">{t(T.comments_form_title)}</h3>
<input
type="text"
<TextInput
bind:value={author}
placeholder={t(T.comments_name_placeholder)}
label=""
required
maxlength="80"
class="w-full p-2 border border-stein-300 rounded-xs text-sm focus:outline-none focus:ring-2 focus:ring-stein-400"
size="sm"
/>
<textarea
bind:value={body}
+23
View File
@@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/sveltekit';
import IconButton from './IconButton.svelte';
const meta = {
title: 'Atoms/IconButton',
component: IconButton,
tags: ['autodocs'],
parameters: { layout: 'centered' },
args: { icon: 'lucide:chevron-right', label: 'Weiter' },
argTypes: {
size: { control: 'select', options: ['sm', 'md'] },
variant: { control: 'select', options: ['surface', 'ghost'] },
disabled: { control: 'boolean' },
},
} satisfies Meta<typeof IconButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Surface: Story = { args: { variant: 'surface' } };
export const Ghost: Story = { args: { variant: 'ghost', icon: 'lucide:x', label: 'Schließen' } };
export const Klein: Story = { args: { variant: 'surface', size: 'sm', icon: 'lucide:chevron-left', label: 'Zurück' } };
export const Disabled: Story = { args: { variant: 'surface', disabled: true } };
+56
View File
@@ -0,0 +1,56 @@
<script lang="ts">
/**
* IconButton — runder Icon-only Button (Carousel-Pfeile, Modal-Close, …).
* Die eine Quelle für kreisförmige Icon-Buttons. `label` = aria-label (Pflicht).
* variant: surface (weiß, Border, Shadow — z.B. Slider-Pfeile) · ghost (transparent).
*/
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
type Size = "sm" | "md";
type Variant = "surface" | "ghost";
let {
icon,
label,
href = undefined,
onclick = undefined,
size = "md",
variant = "surface",
disabled = false,
class: className = "",
...rest
}: {
icon: string;
label: string;
href?: string;
onclick?: (e: MouseEvent) => void;
size?: Size;
variant?: Variant;
disabled?: boolean;
class?: string;
[key: string]: unknown;
} = $props();
const sizeClass: Record<Size, string> = { sm: "size-7", md: "size-9" };
const iconClass: Record<Size, string> = { sm: "size-4", md: "size-5" };
const variantClass: Record<Variant, string> = {
surface:
"border border-stein-200 bg-white text-stein-700 shadow-md hover:bg-stein-50",
ghost: "text-stein-600 hover:bg-stein-100",
};
const cls = $derived(
`inline-flex shrink-0 items-center justify-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${sizeClass[size]} ${variantClass[variant]} ${className}`,
);
</script>
{#if href && !disabled}
<a {href} class={cls} aria-label={label} {...rest}>
<Icon icon={icon} class={iconClass[size]} aria-hidden="true" />
</a>
{:else}
<button type="button" {disabled} {onclick} class={cls} aria-label={label} {...rest}>
<Icon icon={icon} class={iconClass[size]} aria-hidden="true" />
</button>
{/if}
+2 -10
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import IconButton from "$lib/components/IconButton.svelte";
let {
src,
@@ -33,12 +32,5 @@
class="max-w-full max-h-full object-contain rounded shadow-2xl cursor-default"
onclick={(e) => e.stopPropagation()}
/>
<button
type="button"
onclick={onclose}
class="absolute top-4 right-4 p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors"
aria-label="Schließen"
>
<Icon icon="lucide:x" class="size-5" aria-hidden="true" />
</button>
<IconButton icon="lucide:x" label="Schließen" variant="ghost" size="md" onclick={onclose} class="absolute top-4 right-4" />
</div>
+2 -3
View File
@@ -66,7 +66,7 @@
<div class="flex gap-2.5">
{#if icon}<div class="pt-px">{@render iconBadge("size-5", "size-3")}</div>{/if}
<div class="flex min-w-0 flex-1 flex-col gap-2">
<h3 class="text-sm! font-bold! leading-[1.3]! tracking-[-0.01em] text-stein-800">{title}</h3>
<h3 class="text-sm font-bold leading-[1.3] tracking-[-0.01em] text-stein-800">{title}</h3>
{#if description}
<p class="line-clamp-3 text-[13px] font-normal leading-[1.45] text-stein-500">{description}</p>
{/if}
@@ -80,8 +80,7 @@
{:else}
{#if icon}{@render iconBadge("size-11", "size-[23px]")}{/if}
<div class="flex flex-col gap-1">
<!-- text-*! nötig: app.css setzt h3-Größe unlayered → Cascade-Layers. -->
<h3 class="text-base! font-bold! leading-[1.3]! tracking-[-0.01em] text-stein-800">{title}</h3>
<h3 class="text-base font-bold leading-[1.3] tracking-[-0.01em] text-stein-800">{title}</h3>
{#if description}
<p class="line-clamp-3 text-sm font-normal leading-[1.45] text-stein-500">{description}</p>
{/if}
+16 -9
View File
@@ -6,6 +6,7 @@
import EventBadges from "./EventBadges.svelte";
import RustyImage from "./RustyImage.svelte";
import Card from "./Card.svelte";
import Badge from "./Badge.svelte";
import { getPostEventInfo } from "$lib/blog-utils";
import { useTranslate, T } from "$lib/translations";
import { marked } from "$lib/markdown-safe";
@@ -100,28 +101,34 @@
{/if}
{#if isPastEvent}
<div class="absolute top-2 right-2">
<span class="inline-flex items-center gap-1 rounded-full bg-stein-800/90 px-2.5 py-1 text-[.65rem] font-semibold uppercase tracking-wide text-white shadow-sm">
Vergangen
</span>
<Badge variant="solid" color="slate" uppercase class="shadow-sm">Vergangen</Badge>
</div>
{/if}
<div class="absolute top-2 left-2 flex flex-col items-start gap-1">
{#if isNew}
<span
class="inline-flex items-center gap-1 rounded-full bg-wald-600 px-2 py-1 text-[.65rem] font-semibold uppercase tracking-wide text-white shadow-sm"
<Badge
variant="solid"
color="green"
uppercase
class="inline-flex items-center gap-1 shadow-sm"
>
<Icon icon="lucide:sparkles" class="size-3" />
Neu
</span>
</Badge>
{/if}
{#if commentCount != null}
<span
class="inline-flex items-center gap-1 rounded-full bg-white/90 px-2 py-1 text-[.65rem] font-semibold tabular-nums text-stein-700 shadow-sm"
aria-label={t(T.comments_count_aria, { count: commentCount })}
title={t(T.comments_count_aria, { count: commentCount })}
>
<Icon icon="lucide:message-circle" class="size-3" />
{commentCount}
<Badge
variant="solid"
color="white"
class="inline-flex items-center gap-1 tabular-nums shadow-sm"
>
<Icon icon="lucide:message-circle" class="size-3" />
{commentCount}
</Badge>
</span>
{/if}
</div>
@@ -7,6 +7,7 @@
import QrButton from "$lib/components/QrButton.svelte";
import Button from "$lib/ui/Button.svelte";
import Badge from "$lib/components/Badge.svelte";
import SearchField from "$lib/ui/SearchField.svelte";
let { block }: { block: AdressbuchBlockData } = $props();
@@ -123,17 +124,14 @@
<!-- Search + Filter bar -->
<div class="no-print mb-4 flex flex-col gap-2">
<div class="flex gap-2">
<div class="relative max-w-sm flex-1">
<Icon icon="lucide:search" class="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-stein-400" />
<input
type="search"
placeholder="Suchen…"
bind:value={search}
class="w-full rounded-sm border border-stein-200 bg-white py-1.5 pl-8 pr-3 text-sm text-stein-900 placeholder:text-stein-400 focus:border-wald-400 focus:outline-none focus:ring-1 focus:ring-wald-400"
/>
</div>
<SearchField
bind:value={search}
placeholder="Suchen…"
ariaLabel="Kontakte durchsuchen"
class="max-w-sm flex-1"
/>
<!-- View toggle -->
<div class="flex rounded-sm border border-stein-200 overflow-hidden text-[11px] font-medium">
<div class="flex rounded-md border border-stein-200 overflow-hidden text-[11px] font-medium">
<button
type="button"
onclick={() => { viewMode = "alpha"; activeLetter = null; }}
@@ -198,7 +196,7 @@
{#if filtered().length === 0}
<p class="text-sm text-stein-400">Keine Kontakte gefunden.</p>
{:else}
<div class="divide-y divide-stein-100 rounded-sm border border-stein-200 bg-white">
<div class="divide-y divide-stein-100 rounded-lg border border-stein-200 bg-white">
{#if viewMode === "alpha"}
{#each alphaGrouped() as [letter, contacts]}
<div class="sticky top-0 z-10 border-b border-stein-100 bg-stein-50 px-4 py-1 text-[0.65rem] font-semibold tracking-widest text-stein-400">
+9 -18
View File
@@ -17,6 +17,7 @@
import "$lib/iconify-offline";
import Icon from "@iconify/svelte";
import SearchField from "$lib/ui/SearchField.svelte";
import Badge from "$lib/components/Badge.svelte";
import ImageModal from "$lib/components/ImageModal.svelte";
import SocialImageModal from "$lib/components/SocialImageModal.svelte";
import QrModal from "$lib/components/QrModal.svelte";
@@ -660,11 +661,9 @@
{title}
</span>
{#if live}
<span
class="text-[10px] uppercase tracking-wide font-semibold text-fire-50 bg-fire-700 px-1.5 py-0.5 rounded-xs shrink-0"
>
<Badge color="fire" variant="solid" uppercase class="shrink-0">
{live}
</span>
</Badge>
{/if}
</div>
<div
@@ -696,11 +695,7 @@
{/if}
{#if item.tags && item.tags.length > 0}
{#each item.tags.slice(0, 2) as tg}
<span
class="bg-himmel-100 text-himmel-800 text-[10px] px-1.5 py-0.5 rounded-xs"
>
{tg}
</span>
<Badge color="blue">{tg}</Badge>
{/each}
{#if item.tags.length > 2}
<span class="text-[10px] text-stein-500"
@@ -732,11 +727,7 @@
{#if item.tags && item.tags.length > 2}
<div class="flex flex-wrap gap-1">
{#each item.tags as tg}
<span
class="bg-himmel-100 text-himmel-800 text-[10px] px-1.5 py-0.5 rounded-xs"
>
{tg}
</span>
<Badge color="blue">{tg}</Badge>
{/each}
</div>
{/if}
@@ -1001,9 +992,9 @@
/>
{t(T.calendar_filter_by_tag)}
{#if activeTag}
<span class="ml-1 rounded-full bg-himmel-700 px-2 py-0.5 text-[10px] normal-case tracking-normal text-himmel-50">
<Badge color="blue" variant="solid" class="ml-1 normal-case tracking-normal">
{activeTag}
</span>
</Badge>
{/if}
</summary>
<div class="flex flex-wrap items-center gap-1.5 px-4 pb-2 pt-1">
@@ -1115,7 +1106,7 @@
<div class="flex items-center gap-0.5">
<button
type="button"
class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
class="p-2 rounded-md text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
aria-label={t(T.calendar_prev_month)}
onclick={prevMonth}
>
@@ -1127,7 +1118,7 @@
</button>
<button
type="button"
class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
class="p-2 rounded-md text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
aria-label={t(T.calendar_next_month)}
onclick={nextMonth}
>
@@ -91,11 +91,11 @@
</div>
{#if upcoming.length === 0}
<p class="rounded-sm border border-stein-200 px-3 py-6 text-center text-sm text-stein-500">
<p class="rounded-lg border border-stein-200 px-3 py-6 text-center text-sm text-stein-500">
{t(T.calendar_no_future_events)}
</p>
{:else}
<ul class="divide-y divide-stein-100 overflow-hidden rounded-sm border border-stein-200 bg-white">
<ul class="divide-y divide-stein-100 overflow-hidden rounded-lg border border-stein-200 bg-white">
{#each upcoming as { it, start } (it._slug ?? it.title)}
{@const timeStr = fmtTime(start)}
{@const loc = locationText(it)}
@@ -53,7 +53,7 @@
<!-- rechts: Aside -->
<aside class="flex flex-col gap-5">
{#if block.asideTitle}
<h3 class="text-xl! font-bold! leading-tight! tracking-[-0.01em] text-stein-800">
<h3 class="text-xl font-bold leading-tight tracking-[-0.01em] text-stein-800">
{block.asideTitle}
</h3>
{/if}
+3 -2
View File
@@ -3,6 +3,7 @@
import "$lib/iconify-offline";
import { getBlockLayoutClasses } from "$lib/block-layout";
import SectionHeader from "$lib/components/SectionHeader.svelte";
import Badge from "$lib/components/Badge.svelte";
import type { FilesBlockData } from "$lib/block-types";
import {
extractCmsFileField,
@@ -134,7 +135,7 @@
<li class="list-none! ml-0">
<a
href={f.href}
class="group flex items-start gap-2 rounded-xs border border-stein-200 bg-white px-2.5 py-1.5 hover:border-stein-400 hover:bg-stein-50 transition-colors no-underline"
class="group flex items-start gap-2 rounded-lg border border-stein-200 bg-white px-2.5 py-1.5 hover:border-stein-400 hover:bg-stein-50 transition-colors no-underline"
target="_blank"
rel="noopener noreferrer"
>
@@ -143,7 +144,7 @@
<span class="flex items-center gap-1.5 min-w-0">
<span class="text-sm text-stein-900 group-hover:underline truncate">{f.label}</span>
{#if f.sizeLabel}
<span class="shrink-0 text-[.6rem] tabular-nums whitespace-nowrap bg-stein-100 text-stein-500 px-1 py-px rounded-xs">{f.sizeLabel}</span>
<Badge class="tabular-nums">{f.sizeLabel}</Badge>
{/if}
</span>
{#if f.description}
@@ -9,6 +9,7 @@
import { useTranslate } from "$lib/translations";
import { T } from "$lib/translations";
import RustyImage from "$lib/components/RustyImage.svelte";
import IconButton from "$lib/components/IconButton.svelte";
import { extractCmsImageField } from "$lib/rusty-image";
const t = useTranslate();
@@ -256,31 +257,10 @@
</a>
</div>
{#if withUrl.length > 1}
<button
type="button"
class="absolute left-2 top-1/2 -translate-y-1/2 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white/50"
onclick={modalPrev}
aria-label={t(T.image_gallery_prev_aria)}
>
<Icon icon="lucide:chevron-left" class="text-2xl" aria-hidden="true" />
</button>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white/50"
onclick={modalNext}
aria-label={t(T.image_gallery_next_aria)}
>
<Icon icon="lucide:chevron-right" class="text-2xl" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label={t(T.image_gallery_prev_aria)} variant="surface" size="md" onclick={modalPrev} class="absolute left-2 top-1/2 -translate-y-1/2" />
<IconButton icon="lucide:chevron-right" label={t(T.image_gallery_next_aria)} variant="surface" size="md" onclick={modalNext} class="absolute right-2 top-1/2 -translate-y-1/2" />
{/if}
<button
type="button"
class="absolute -top-2 -right-2 inline-flex h-8 w-8 items-center justify-center rounded-full bg-white text-stein-900 shadow-md hover:bg-stein-100 focus:outline-none focus:ring-2 focus:ring-white/50"
onclick={closeModal}
aria-label={t(T.image_gallery_close)}
>
<Icon icon="lucide:x" class="text-lg" aria-hidden="true" />
</button>
<IconButton icon="lucide:x" label={t(T.image_gallery_close)} variant="ghost" size="md" onclick={closeModal} class="absolute -top-2 -right-2" />
</div>
</div>
{/if}
@@ -344,22 +324,8 @@
{/key}
</div>
<button
type="button"
class="absolute left-2 top-1/2 -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-opacity focus:outline-none focus:ring-2 focus:ring-wald-500 lg:opacity-0 lg:group-hover:opacity-100"
aria-label={t(T.image_gallery_prev_aria)}
onclick={prev}
>
<Icon icon="lucide:chevron-left" class="text-2xl" aria-hidden="true" />
</button>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-opacity focus:outline-none focus:ring-2 focus:ring-wald-500 lg:opacity-0 lg:group-hover:opacity-100"
aria-label={t(T.image_gallery_next_aria)}
onclick={next}
>
<Icon icon="lucide:chevron-right" class="text-2xl" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label={t(T.image_gallery_prev_aria)} variant="surface" size="sm" onclick={prev} class="absolute left-2 top-1/2 -translate-y-1/2 transition-opacity lg:opacity-0 lg:group-hover:opacity-100" />
<IconButton icon="lucide:chevron-right" label={t(T.image_gallery_next_aria)} variant="surface" size="sm" onclick={next} class="absolute right-2 top-1/2 -translate-y-1/2 transition-opacity lg:opacity-0 lg:group-hover:opacity-100" />
<div class="flex justify-center gap-1.5 mt-2" role="tablist" aria-label={t(T.image_gallery_position_aria)}>
{#each withUrl as _, i}
@@ -12,6 +12,7 @@
import SectionHeader from "$lib/components/SectionHeader.svelte";
import SectionGrid from "$lib/components/SectionGrid.svelte";
import InfoCard from "$lib/components/InfoCard.svelte";
import IconButton from "$lib/components/IconButton.svelte";
function organisationLogoField(o: OrganisationEntry) {
return extractCmsImageField(o.logo);
@@ -235,22 +236,8 @@
{/each}
</div>
{#if orgs.length > 2}
<button
type="button"
onclick={() => scrollByPage(-1)}
aria-label="Vorherige Organisationen"
class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2 size-7 items-center justify-center rounded-full bg-white border border-stein-200 shadow-md hover:bg-stein-50 transition"
>
<Icon icon="lucide:chevron-left" class="size-4" aria-hidden="true" />
</button>
<button
type="button"
onclick={() => scrollByPage(1)}
aria-label="Weitere Organisationen"
class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2 size-7 items-center justify-center rounded-full bg-white border border-stein-200 shadow-md hover:bg-stein-50 transition"
>
<Icon icon="lucide:chevron-right" class="size-4" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label="Vorherige Organisationen" variant="surface" size="sm" onclick={() => scrollByPage(-1)} class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2" />
<IconButton icon="lucide:chevron-right" label="Weitere Organisationen" variant="surface" size="sm" onclick={() => scrollByPage(1)} class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2" />
{/if}
</div>
{#if showAddOrganisationCard}
@@ -144,7 +144,7 @@
{/if}
<div
class="relative h-[320px] overflow-hidden rounded-sm border border-stein-200 sm:h-[clamp(400px,65vh,560px)]"
class="relative h-[320px] overflow-hidden rounded-lg border border-stein-200 sm:h-[clamp(400px,65vh,560px)]"
>
<!-- Leaflet map fills the container -->
<div bind:this={mapEl} class="absolute inset-0"></div>
@@ -2,8 +2,7 @@
import { marked } from "$lib/markdown-safe";
import PostCard from "../PostCard.svelte";
import PostCardCompact from "../PostCardCompact.svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import IconButton from "$lib/components/IconButton.svelte";
import { getBlockLayoutClasses } from "$lib/block-layout";
import SectionHeader from "$lib/components/SectionHeader.svelte";
import ArrowLink from "$lib/components/ArrowLink.svelte";
@@ -133,22 +132,8 @@
{/each}
</div>
{#if posts.length > 1}
<button
type="button"
onclick={() => scrollByPage(-1)}
aria-label="Zurück"
class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2 size-7 items-center justify-center rounded-full bg-white/90 border border-stein-200 shadow-sm hover:bg-white hover:border-wald-700 transition"
>
<Icon icon="lucide:chevron-left" class="size-4" aria-hidden="true" />
</button>
<button
type="button"
onclick={() => scrollByPage(1)}
aria-label="Weiter"
class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2 size-7 items-center justify-center rounded-full bg-white/90 border border-stein-200 shadow-sm hover:bg-white hover:border-wald-700 transition"
>
<Icon icon="lucide:chevron-right" class="size-4" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label="Zurück" variant="surface" size="sm" onclick={() => scrollByPage(-1)} class="hidden md:flex absolute -left-3 top-1/2 -translate-y-1/2 -translate-x-1/2" />
<IconButton icon="lucide:chevron-right" label="Weiter" variant="surface" size="sm" onclick={() => scrollByPage(1)} class="hidden md:flex absolute -right-3 top-1/2 -translate-y-1/2 translate-x-1/2" />
{/if}
</div>
{/if}
@@ -2,8 +2,7 @@
import { getBlockLayoutClasses } from "$lib/block-layout";
import type { QuoteCarouselBlockData } from "$lib/block-types";
import { onMount, onDestroy } from "svelte";
import Icon from "@iconify/svelte";
import "$lib/iconify-offline";
import IconButton from "$lib/components/IconButton.svelte";
type QuoteItem = { quote?: string; author?: string };
@@ -149,22 +148,8 @@
{/each}
</div>
<div class="flex items-center gap-2">
<button
type="button"
onclick={() => go(index - 1)}
aria-label="Vorheriges Zitat"
class="inline-flex size-9 items-center justify-center rounded-full border border-wald-600 text-wald-200 transition hover:border-wald-300 hover:text-white"
>
<Icon icon="lucide:chevron-left" class="size-4" aria-hidden="true" />
</button>
<button
type="button"
onclick={() => go(index + 1)}
aria-label="Nächstes Zitat"
class="inline-flex size-9 items-center justify-center rounded-full border border-wald-600 text-wald-200 transition hover:border-wald-300 hover:text-white"
>
<Icon icon="lucide:chevron-right" class="size-4" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label="Vorheriges Zitat" variant="surface" size="md" onclick={() => go(index - 1)} />
<IconButton icon="lucide:chevron-right" label="Nächstes Zitat" variant="surface" size="md" onclick={() => go(index + 1)} />
</div>
</div>
{/if}
@@ -10,6 +10,8 @@
} from "$lib/block-types";
import "$lib/iconify-offline";
import Icon from "@iconify/svelte";
import SearchField from "$lib/ui/SearchField.svelte";
import Badge from "$lib/components/Badge.svelte";
import { t as tStatic, T } from "$lib/translations";
import type { Translations } from "$lib/translations";
@@ -220,26 +222,12 @@
class="sticky top-14 z-10 px-4 md:px-6 py-3 border-b border-stein-200 bg-white/95 backdrop-blur-sm shadow-sm"
>
<div class="flex flex-col gap-2">
<div class="relative max-w-sm flex-1">
<Icon icon="lucide:search" class="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-stein-400 pointer-events-none" aria-hidden="true" />
<input
type="search"
placeholder={t(T.searchable_text_placeholder)}
class="w-full rounded-sm border border-stein-200 bg-white py-1.5 pl-8 pr-8 text-sm text-stein-900 placeholder:text-stein-400 focus:border-wald-400 focus:outline-none focus:ring-1 focus:ring-wald-400"
bind:value={searchQuery}
aria-label={t(T.searchable_text_search_aria)}
/>
{#if searchQuery}
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 text-stein-400 hover:text-stein-700 transition-colors"
onclick={() => (searchQuery = "")}
aria-label={t(T.searchable_text_clear_search)}
>
<Icon icon="lucide:x" class="size-4" aria-hidden="true" />
</button>
{/if}
</div>
<SearchField
bind:value={searchQuery}
placeholder={t(T.searchable_text_placeholder)}
ariaLabel={t(T.searchable_text_search_aria)}
class="max-w-sm flex-1"
/>
{#if allTags.length > 0}
<div class="flex flex-wrap gap-1">
<button
@@ -287,7 +275,7 @@
</div>
<button
type="button"
class="text-xs px-3 py-1.5 rounded-xs border border-stein-200 bg-white text-stein-600 hover:bg-wald-50 hover:border-wald-300 hover:text-wald-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500"
class="text-xs px-3 py-1.5 rounded-md border border-stein-200 bg-white text-stein-600 hover:bg-wald-50 hover:border-wald-300 hover:text-wald-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500"
onclick={() => { searchQuery = ""; selectedTag = "all"; }}
>
Filter zurücksetzen
@@ -339,7 +327,7 @@
{#if fragment.tags.length > 0}
<div class="flex flex-wrap gap-1">
{#each fragment.tags as tag}
<span class="inline-flex items-center whitespace-nowrap rounded-xs border border-stein-200 bg-stein-100 px-1.5 py-0 text-[0.65rem] font-medium text-stein-600">{tag}</span>
<Badge>{tag}</Badge>
{/each}
</div>
{/if}
@@ -8,6 +8,9 @@
import type { TextFragment, WindArea } from "$lib/windkarte";
import "$lib/iconify-offline";
import Icon from "@iconify/svelte";
import Button from "$lib/ui/Button.svelte";
import Badge from "$lib/components/Badge.svelte";
import Checkbox from "$lib/ui/Checkbox.svelte";
let {
block,
@@ -805,11 +808,11 @@ ${outputText}
<span class="text-wald-800"><Icon icon="lucide:save" class="mr-1 inline size-4" />Letzte Sitzung wiederhergestellt.</span>
<div class="flex gap-2">
{#if outputHistory.length > 0}
<button class="rounded-md bg-wald-700 px-3 py-1.5 text-xs font-semibold text-white hover:bg-wald-800" onclick={() => goTo(4)}>Zur Ausgabe →</button>
<Button variant="primary" size="sm" onclick={() => goTo(4)}>Zur Ausgabe →</Button>
{:else if selectedSlugs.size > 0}
<button class="rounded-md bg-wald-700 px-3 py-1.5 text-xs font-semibold text-white hover:bg-wald-800" onclick={() => goTo(3)}>Fortsetzen →</button>
<Button variant="primary" size="sm" onclick={() => goTo(3)}>Fortsetzen →</Button>
{/if}
<button class="rounded-md border border-stein-300 px-3 py-1.5 text-xs text-stein-600 hover:bg-stein-50" onclick={resetAll}>Neu starten</button>
<Button variant="secondary" size="sm" onclick={resetAll}>Neu starten</Button>
</div>
</div>
{/if}
@@ -822,7 +825,7 @@ ${outputText}
{/if}
<!-- 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="stellingnahme-stepper sticky top-2 z-[1100] -mx-3 mb-6 rounded-lg 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
@@ -867,7 +870,7 @@ ${outputText}
<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">
<div class="mb-6 rounded-lg border border-wald-200 bg-wald-50 p-5">
<div class="mb-2 text-xs font-medium uppercase tracking-wide text-wald-600">Vorranggebiet</div>
<div class="text-4xl font-bold tracking-tight text-wald-900">{windArea.gebiets_nr}</div>
{#if windArea.bezeichnung}
@@ -916,7 +919,7 @@ ${outputText}
<!-- Mini-map -->
{#if browser && MapComponent && mapReady && mapAreaData.length > 0}
<div class="relative mb-6 aspect-square overflow-hidden rounded-xl border border-stein-200 shadow-sm sm:aspect-auto sm:h-80">
<div class="relative mb-6 aspect-square overflow-hidden rounded-lg border border-stein-200 shadow-sm sm:aspect-auto sm:h-80">
{#await MapComponent then Map}
<Map
areas={mapAreaData}
@@ -944,13 +947,13 @@ ${outputText}
{:else if mapReady && mapAreaData.length === 0}
<!-- no geometry available, skip silently -->
{:else if !mapReady}
<div class="mb-6 flex h-40 items-center justify-center rounded-xl border border-stein-200 bg-stein-50 text-sm text-stein-400">
<div class="mb-6 flex h-40 items-center justify-center rounded-lg border border-stein-200 bg-stein-50 text-sm text-stein-400">
Karte wird geladen …
</div>
{/if}
{:else}
<div class="mb-6 rounded-xl border border-erde-200 bg-erde-50 p-4 text-sm text-erde-700">
<div class="mb-6 rounded-lg border border-erde-200 bg-erde-50 p-4 text-sm text-erde-700">
Kein Vorranggebiet konfiguriert.
</div>
{/if}
@@ -961,11 +964,7 @@ ${outputText}
Sie wählen zuerst ortskonkrete Argumente für Ihr Gebiet, dann allgemeine Einwände.
</p>
<div class="sticky bottom-2 z-10 flex flex-wrap items-center 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 bg-wald-700 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-wald-800 disabled:opacity-40"
disabled={!windArea}
onclick={() => goTo(1)}
>Weiter zu den Argumenten →</button>
<Button variant="primary" disabled={!windArea} onclick={() => goTo(1)}>Weiter zu den Argumenten →</Button>
{#if selectedSlugs.size > 0 || absenderName}
<button class="text-xs text-stein-400 hover:text-stein-600" onclick={resetAll}>Neu starten</button>
{/if}
@@ -1024,20 +1023,20 @@ ${outputText}
{/each}
</div>
{:else}
<div class="mb-6 rounded-xl border border-erde-200 bg-erde-50 p-4 text-sm text-erde-700">
<div class="mb-6 rounded-lg border border-erde-200 bg-erde-50 p-4 text-sm text-erde-700">
Für dieses Gebiet sind noch keine ortskonkreten Argumente hinterlegt.
</div>
{/if}
<div class="sticky bottom-2 z-10 flex items-center 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(0)}> Zurück</button>
<button
class="rounded-lg bg-wald-700 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-wald-800"
<Button variant="secondary" onclick={() => goTo(0)}> Zurück</Button>
<Button
variant="primary"
onclick={() => goTo(2)}
title={ortskonkreteSelectedCount === 0 ? "Tipp: Mindestens ein Argument wählen stärkt Ihre Einwendung" : undefined}
>
Weiter{ortskonkreteSelectedCount > 0 ? ` (${ortskonkreteSelectedCount} gewählt)` : ""}
</button>
</Button>
</div>
</div>
@@ -1164,20 +1163,20 @@ ${outputText}
{/each}
</div>
{:else}
<div class="mb-6 rounded-xl border border-erde-200 bg-erde-50 p-4 text-sm text-erde-700">
<div class="mb-6 rounded-lg border border-erde-200 bg-erde-50 p-4 text-sm text-erde-700">
Keine allgemeinen Argumente hinterlegt.
</div>
{/if}
<div class="sticky bottom-2 z-10 flex items-center 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(1)}> Zurück</button>
<button
class="rounded-lg bg-wald-700 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-wald-800"
<Button variant="secondary" onclick={() => goTo(1)}> Zurück</Button>
<Button
variant="primary"
onclick={() => goTo(3)}
title={selectedFragments.length === 0 ? "Tipp: Ohne Argumente wird die Einwendung sehr kurz" : undefined}
>
Weiter{selectedFragments.length > 0 ? ` (${selectedFragments.length} Argument${selectedFragments.length !== 1 ? "e" : ""})` : ""}
</button>
</Button>
</div>
</div>
@@ -1211,12 +1210,12 @@ ${outputText}
<span class="tabular-nums">{persoenlicheEingabe.length} Zeichen</span>
</p>
<div class="sticky bottom-2 z-10 mt-5 flex items-center 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(2)}> Zurück</button>
<button
class="rounded-lg bg-wald-700 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-wald-800"
<Button variant="secondary" onclick={() => goTo(2)}> Zurück</Button>
<Button
variant="primary"
onclick={() => goTo(4)}
title={!persoenlicheEingabe.trim() ? "Tipp: Persönliche Betroffenheit stärkt Ihre Einwendung erheblich" : undefined}
>Weiter →</button>
>Weiter →</Button>
</div>
</div>
@@ -1227,9 +1226,9 @@ ${outputText}
<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"
<Button
variant="secondary"
size="sm"
onclick={() => {
absenderName = "[Vor- und Nachname]";
absenderAnschrift = "[Straße und Hausnummer]";
@@ -1239,7 +1238,7 @@ ${outputText}
>
<Icon icon="lucide:type" class="size-3.5 text-stein-500" />
Mit Platzhaltern füllen
</button>
</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>
@@ -1281,59 +1280,47 @@ ${outputText}
</div>
<!-- Block 1: Schreibvorlage -->
<div class="mb-4 rounded-xl border border-wald-200 bg-white p-4 shadow-sm">
<div class="mb-4 rounded-lg border border-wald-200 bg-white p-4 shadow-sm">
<div class="mb-2 flex items-start gap-2">
<Icon icon="lucide:file-pen" class="mt-0.5 size-5 shrink-0 text-wald-700" />
<div class="flex-1">
<div class="flex flex-wrap items-center gap-2">
<h4 class="text-sm font-semibold text-wald-800">Fertige Schreibvorlage</h4>
<span class="rounded-full bg-wald-600 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-white">Empfohlen</span>
<Badge color="green" variant="solid" uppercase>Empfohlen</Badge>
</div>
<p class="mt-0.5 text-xs text-stein-500">Direkt nutzbar: kopieren, herunterladen, drucken oder per E-Mail senden.</p>
</div>
</div>
<div class="flex flex-wrap gap-2">
<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}
>
<Button variant="secondary" size="sm" onclick={copyText}>
<Icon icon="lucide: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}
>
</Button>
<Button variant="secondary" size="sm" onclick={downloadText}>
<Icon icon="lucide: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}
>
</Button>
<Button variant="secondary" size="sm" onclick={printText}>
<Icon icon="lucide:printer" class="size-3.5" />
Drucken
</button>
</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"
>
<Button variant="secondary" size="sm" href={mailtoLink}>
<Icon icon="lucide:mail" class="size-3.5" />
{block.recipientEmail ? "Per E-Mail senden" : "Per E-Mail (an mich)"}
</a>
</Button>
{/if}
</div>
</div>
<!-- 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-4 rounded-lg border border-himmel-200 bg-himmel-50/40 p-4 shadow-sm">
<div class="mb-3 flex items-start gap-2">
<Icon icon="lucide:bot" class="mt-0.5 size-5 shrink-0 text-himmel-700" />
<div class="flex-1">
<div class="flex flex-wrap items-center gap-2">
<h4 class="text-sm font-semibold text-himmel-800">Individueller mit KI</h4>
<span class="rounded-full bg-himmel-200 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-himmel-800">Optional</span>
<Badge color="blue" uppercase>Optional</Badge>
</div>
<p class="mt-0.5 text-xs text-stein-500">
Wer mag: die Vorlage von ChatGPT, Claude o.ä. zu persönlichem Fließtext umformulieren lassen.
@@ -1392,7 +1379,7 @@ ${outputText}
<Icon icon="lucide:sliders-horizontal" 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>
<Badge color="blue">angepasst</Badge>
{/if}
</span>
<Icon icon="lucide:chevron-down" class="det-chevron size-4 text-himmel-600" />
@@ -1419,21 +1406,19 @@ ${outputText}
</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}
>
<Button variant="tertiary" size="sm" onclick={copyAiPrompt}>
<Icon icon="lucide: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"
</Button>
<Button
variant="secondary"
size="sm"
onclick={downloadAiPrompt}
title="Prompt als Textdatei speichern, alternativ zur Zwischenablage."
>
<Icon icon="lucide:download" class="size-3.5" />
Als .txt herunterladen
</button>
</Button>
</div>
<!-- An das Team senden: Prompt landet zur Weiterverarbeitung in RustyCMS -->
@@ -1479,30 +1464,34 @@ ${outputText}
</div>
</div>
<label class="mt-2 flex items-start gap-2 text-xs text-stein-600">
<input type="checkbox" bind:checked={sendConsent} class="mt-0.5 shrink-0 accent-wald-600" />
<span>Ich bin einverstanden, dass die oben genannten Angaben zur Weiterverarbeitung an das Team von windwiderstand.de übermittelt werden.</span>
</label>
<div class="mt-2">
<Checkbox
bind:checked={sendConsent}
size="sm"
label="Ich bin einverstanden, dass die oben genannten Angaben zur Weiterverarbeitung an das Team von windwiderstand.de übermittelt werden."
/>
</div>
{#if sendError}
<p class="mt-2 text-xs text-fire-600">{sendError}</p>
{/if}
<button
type="button"
class="mt-3 inline-flex items-center gap-1.5 rounded-md border border-wald-600 bg-wald-700 px-3.5 py-2 text-xs font-semibold text-white shadow-sm hover:bg-wald-800 disabled:opacity-50"
<Button
variant="primary"
size="sm"
class="mt-3"
onclick={sendToTeam}
disabled={sendState === "sending"}
>
<Icon icon={sendState === "sending" ? "lucide:loader-circle" : "lucide:send"} class="size-3.5 {sendState === 'sending' ? 'animate-spin' : ''}" />
{sendState === "sending" ? "Wird gesendet …" : "An das Team senden"}
</button>
</Button>
{/if}
</div>
</div>
<!-- Brief im Detail anpassen, alle Feintuning-Optionen kompakt -->
<details class="mb-4 rounded-xl border border-stein-200 bg-white overflow-hidden">
<details class="mb-4 rounded-lg 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="lucide:sliders-horizontal" class="size-4 text-stein-500" />
@@ -1521,7 +1510,7 @@ ${outputText}
<Icon icon="lucide:file-text" 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>
<Badge color="blue">bearbeitet</Badge>
{/if}
</p>
<textarea
@@ -1534,10 +1523,7 @@ ${outputText}
{#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>
<Button variant="secondary" size="sm" onclick={generateOutput}> Neu aus Vorlage</Button>
</div>
{/if}
{#if mailTooLong}
@@ -1612,8 +1598,8 @@ ${outputText}
<!-- 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}>
<Button variant="secondary" onclick={() => goTo(3)}> Zurück</Button>
<button class="rounded-md border border-erde-300 px-4 py-2 text-sm text-erde-700 hover:bg-erde-50" onclick={resetAll}>
↻ Neustart
</button>
</div>
+14 -14
View File
@@ -375,7 +375,7 @@
<svelte:element
this={explanationHref ? "a" : "div"}
href={explanationHref || undefined}
class="block rounded-xs border border-l-4 bg-white shadow-sm {accentClasses.box} {explanationHref ? 'no-underline! hover:brightness-95' : ''}"
class="block rounded-lg border border-l-4 bg-white shadow-sm {accentClasses.box} {explanationHref ? 'no-underline! hover:brightness-95' : ''}"
>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 px-4 py-3 text-sm">
{#if !live}
@@ -423,7 +423,7 @@
{/if}
<div
class="rounded-xs border-2 shadow-sm overflow-hidden {accentClasses.box}"
class="rounded-lg border-2 shadow-sm overflow-hidden {accentClasses.box}"
>
<!-- Header bar — title + source badge + last-updated stamp.
Source-Badge zeigt klar woher die zahlen kommen ohne dass
@@ -479,7 +479,7 @@
</span>
<button
type="button"
class="inline-flex items-center gap-1 rounded-xs border border-fire-300 bg-fire-100 px-3 py-1 text-xs font-semibold text-fire-800 hover:bg-fire-200 disabled:opacity-50"
class="inline-flex items-center gap-1 rounded-md border border-fire-300 bg-fire-100 px-3 py-1 text-xs font-semibold text-fire-800 hover:bg-fire-200 disabled:opacity-50"
onclick={() => loadLive()}
disabled={isLoading}
>
@@ -565,7 +565,7 @@
brechen. -->
{#if live && !isHero}
<div class="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="rounded-xs border border-stein-200 bg-white px-4 py-2.5 text-sm shadow-sm">
<div class="card-surface px-4 py-2.5 text-sm">
<div class="text-xs text-stein-600 uppercase tracking-wide">
{t(T.strommix_conventional_now)}
</div>
@@ -576,7 +576,7 @@
{t(T.strommix_conventional_formula)}
</div>
</div>
<div class="rounded-xs border border-stein-200 bg-white px-4 py-2.5 text-sm shadow-sm">
<div class="card-surface px-4 py-2.5 text-sm">
<div class="text-xs text-stein-600 uppercase tracking-wide">
{importDirection === "Import"
? t(T.strommix_import)
@@ -613,7 +613,7 @@
aussagekräftiger marktzustand der nicht versteckt werden
sollte. Ersetzt die separate Price-Card komplett, weil
derselbe Preis sonst zweimal genannt würde. -->
<div class="mt-2 flex items-start gap-3 rounded-xs border-2 border-fire-500 bg-fire-50 px-4 py-2.5 text-sm shadow-sm">
<div class="mt-2 flex items-start gap-3 rounded-lg border-2 border-fire-500 bg-fire-50 px-4 py-2.5 text-sm shadow-sm">
<Icon
icon="lucide:circle-alert"
class="size-6 shrink-0 text-fire-700"
@@ -650,7 +650,7 @@
Vergleich + Slot-Info sauber nebeneinander stehen. Bei
Negativpreisen unterdrückt; der Negativbanner oben zeigt
dann denselben Preis prominenter. -->
<div class="mt-2 rounded-xs border border-stein-200 bg-white px-4 py-2 text-sm shadow-sm">
<div class="mt-2 card-surface px-4 py-2 text-sm">
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
<span class="font-semibold">
{t(T.strommix_price_de, { price: fmtPrice(live.priceDeEurMwh) })}
@@ -678,13 +678,13 @@
Default zu — Hauptkennzahlen oben sollen dominieren. -->
{#if live && hasHistoryPanels}
<details class="strommix-history mt-3 group">
<summary class="cursor-pointer rounded-xs border border-stein-200 bg-white px-4 py-2 text-sm font-semibold text-stein-700 shadow-sm hover:bg-stein-50 inline-flex items-center gap-2">
<summary class="cursor-pointer rounded-lg border border-stein-200 bg-white px-4 py-2 text-sm font-semibold text-stein-700 shadow-sm hover:bg-stein-50 inline-flex items-center gap-2">
<Icon icon="lucide:chevron-right" class="size-4 transition-transform group-open:rotate-90" aria-hidden="true" />
{t(T.strommix_history_summary)}
</summary>
{#if live.todayHourly && live.todayHourly.length > 0}
<div class="mt-2 rounded-xs border border-stein-200 bg-white px-4 py-2.5 shadow-sm">
<div class="mt-2 card-surface px-4 py-2.5">
<h4 class="text-sm font-semibold text-stein-800">
{t(T.strommix_residual_today_title)}
</h4>
@@ -698,7 +698,7 @@
{/if}
{#if live.lastDunkelflaute || (live.worstDunkelflauten && live.worstDunkelflauten.length > 0)}
<div class="mt-2 rounded-xs border border-stein-200 bg-white px-4 py-2.5 shadow-sm">
<div class="mt-2 card-surface px-4 py-2.5">
<DunkelflauteCounter
last={live.lastDunkelflaute}
worst={live.worstDunkelflauten}
@@ -707,7 +707,7 @@
{/if}
{#if live.negPriceHoursYtd != null || typeof block.einsman_costs_ytd_eur_mio === "number"}
<div class="mt-2 rounded-xs border border-stein-200 bg-white px-4 py-2.5 shadow-sm">
<div class="mt-2 card-surface px-4 py-2.5">
<NegPriceCounter
negHoursYtd={live.negPriceHoursYtd}
einsmanEurMio={block.einsman_costs_ytd_eur_mio ?? null}
@@ -717,7 +717,7 @@
{/if}
{#if live.crossborderTodayGwh || live.netCrossborderGwhYtd != null}
<div class="mt-2 rounded-xs border border-stein-200 bg-white px-4 py-2.5 shadow-sm">
<div class="mt-2 card-surface px-4 py-2.5">
<FlowBalanceTodayYtd
today={live.crossborderTodayGwh}
ytd={live.netCrossborderGwhYtd != null && live.crossborderByCountryGwhYtd
@@ -728,7 +728,7 @@
{/if}
{#if block.installed_wind_onshore_gw || block.installed_solar_gw}
<div class="mt-2 rounded-xs border border-stein-200 bg-white px-4 py-2.5 shadow-sm">
<div class="mt-2 card-surface px-4 py-2.5">
<CapacityFactorBars
windOnshoreLiveMw={live.windOnshoreMw}
windOffshoreLiveMw={live.windOffshoreMw}
@@ -759,7 +759,7 @@
Mapping pro Zelle und wirkt unruhig. Auf sm doppelt: 2
Paare nebeneinander. -->
<div
class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-xs border border-stein-200 bg-stein-50 px-3 py-2 tabular-nums sm:grid-cols-[auto_1fr_auto_1fr] sm:gap-x-4"
class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-lg border border-stein-200 bg-stein-50 px-3 py-2 tabular-nums sm:grid-cols-[auto_1fr_auto_1fr] sm:gap-x-4"
>
<div class="text-stein-500">
{t(T.strommix_field_wind_onshore)}
@@ -11,6 +11,7 @@
import Icon from "@iconify/svelte";
import { useTranslate } from "$lib/translations";
import { T } from "$lib/translations";
import IconButton from "$lib/components/IconButton.svelte";
const t = useTranslate();
let { block }: { block: YoutubeVideoGalleryBlockData } = $props();
@@ -256,31 +257,10 @@
</div>
</div>
{#if items.length > 1}
<button
type="button"
class="absolute left-2 top-1/2 -translate-y-1/2 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white/50"
onclick={modalPrev}
aria-label={t(T.image_gallery_prev_aria)}
>
<Icon icon="lucide:chevron-left" class="text-2xl" aria-hidden="true" />
</button>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 inline-flex h-9 w-9 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white/50"
onclick={modalNext}
aria-label={t(T.image_gallery_next_aria)}
>
<Icon icon="lucide:chevron-right" class="text-2xl" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label={t(T.image_gallery_prev_aria)} variant="surface" size="md" onclick={modalPrev} class="absolute left-2 top-1/2 -translate-y-1/2" />
<IconButton icon="lucide:chevron-right" label={t(T.image_gallery_next_aria)} variant="surface" size="md" onclick={modalNext} class="absolute right-2 top-1/2 -translate-y-1/2" />
{/if}
<button
type="button"
class="absolute -top-2 -right-2 inline-flex h-8 w-8 items-center justify-center rounded-full bg-white text-stein-900 shadow-md hover:bg-stein-100 focus:outline-none focus:ring-2 focus:ring-white/50"
onclick={closeModal}
aria-label={t(T.image_gallery_close)}
>
<Icon icon="lucide:x" class="text-lg" aria-hidden="true" />
</button>
<IconButton icon="lucide:x" label={t(T.image_gallery_close)} variant="ghost" size="md" onclick={closeModal} class="absolute -top-2 -right-2" />
</div>
</div>
{/if}
@@ -317,22 +297,8 @@
</div>
{#if items.length > 1}
<button
type="button"
class="absolute left-2 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-opacity focus:outline-none focus:ring-2 focus:ring-wald-500 lg:opacity-0 lg:group-hover:opacity-100"
aria-label={t(T.image_gallery_prev_aria)}
onclick={prev}
>
<Icon icon="lucide:chevron-left" class="text-2xl" aria-hidden="true" />
</button>
<button
type="button"
class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 transition-opacity focus:outline-none focus:ring-2 focus:ring-wald-500 lg:opacity-0 lg:group-hover:opacity-100"
aria-label={t(T.image_gallery_next_aria)}
onclick={next}
>
<Icon icon="lucide:chevron-right" class="text-2xl" aria-hidden="true" />
</button>
<IconButton icon="lucide:chevron-left" label={t(T.image_gallery_prev_aria)} variant="surface" size="md" onclick={prev} class="absolute left-2 top-1/2 -translate-y-1/2 transition-opacity lg:opacity-0 lg:group-hover:opacity-100" />
<IconButton icon="lucide:chevron-right" label={t(T.image_gallery_next_aria)} variant="surface" size="md" onclick={next} class="absolute right-2 top-1/2 -translate-y-1/2 transition-opacity lg:opacity-0 lg:group-hover:opacity-100" />
{/if}
</div>
@@ -38,7 +38,7 @@
<div class="grid gap-2 sm:grid-cols-2">
{#if showNeg && negHoursYtd != null}
<div class="rounded-xs border-l-4 border-fire-400 bg-white px-3 py-2.5 shadow-sm">
<div class="rounded-lg border-l-4 border-fire-400 bg-white px-3 py-2.5 shadow-sm">
<div class="flex items-baseline gap-1.5">
<span class="text-2xl font-bold tabular-nums text-stein-900">
{fmtNum(negHoursYtd)}
@@ -53,7 +53,7 @@
{/if}
{#if showEinsman && einsmanEurMio != null}
<div class="rounded-xs border-l-4 border-fire-400 bg-white px-3 py-2.5 shadow-sm">
<div class="rounded-lg border-l-4 border-fire-400 bg-white px-3 py-2.5 shadow-sm">
<div class="flex items-baseline gap-1.5">
<span class="text-2xl font-bold tabular-nums text-stein-900">
{fmtNum(einsmanEurMio, 0)}
@@ -147,7 +147,7 @@
bind:value={email}
onfocus={armTurnstile}
aria-label={t(T.newsletter_label_email)}
class="min-w-0 flex-1 rounded-sm border border-stein-700 bg-stein-800 px-3 py-2 text-sm text-stein-0 placeholder:text-stein-400 focus:border-wald-400 focus:outline-none"
class="min-w-0 flex-1 rounded-md border border-stein-700 bg-stein-800 px-3 py-2 text-sm text-stein-0 placeholder:text-stein-400 focus:border-wald-400 focus:outline-none"
/>
<Button
type="submit"
@@ -13,6 +13,7 @@
import QrModal from "$lib/components/QrModal.svelte";
import SocialImageModal from "$lib/components/SocialImageModal.svelte";
import ImageModal from "$lib/components/ImageModal.svelte";
import Badge from "$lib/components/Badge.svelte";
import Button from "$lib/ui/Button.svelte";
let { data }: { data: PageData } = $props();
@@ -133,13 +134,9 @@
<div class="mb-3 flex flex-wrap items-center gap-2">
<span class="text-xs font-semibold uppercase tracking-wider text-wald-700">Termin</span>
{#if relLabel}
<span
class="rounded-full px-2 py-0.5 text-xs font-semibold {isPast
? 'bg-stein-100 text-stein-500'
: 'bg-wald-100 text-wald-800'}"
>
<Badge color={isPast ? "" : "green"}>
{relLabel}
</span>
</Badge>
{/if}
</div>
@@ -180,7 +177,7 @@
{#if item.tags.length > 0}
<div class="mt-4 flex flex-wrap gap-1.5">
{#each item.tags as tg}
<span class="rounded-full bg-himmel-100 px-2.5 py-0.5 text-xs text-himmel-800">{tg}</span>
<Badge color="blue">{tg}</Badge>
{/each}
</div>
{/if}
+33 -24
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { onMount } from "svelte";
import Button from "$lib/ui/Button.svelte";
import TextInput from "$lib/ui/TextInput.svelte";
import Textarea from "$lib/ui/Textarea.svelte";
let title = $state("");
let terminZeit = $state(""); // datetime-local
@@ -86,36 +88,43 @@
<label>Website<input type="text" tabindex="-1" autocomplete="off" bind:value={website} /></label>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-stein-700" for="t-title">Titel <span class="text-fire-600">*</span></label>
<input id="t-title" type="text" required bind:value={title} placeholder="z. B. Einwohnerversammlung Schleusingen" class="w-full rounded-md border border-stein-300 px-3 py-2 text-sm focus:border-wald-500 focus:outline-none focus:ring-2 focus:ring-wald-200" />
</div>
<TextInput
label="Titel"
type="text"
required
bind:value={title}
placeholder="z. B. Einwohnerversammlung Schleusingen"
size="sm"
/>
<div class="grid gap-4 sm:grid-cols-2">
<div>
<label class="mb-1 block text-sm font-medium text-stein-700" for="t-start">Beginn <span class="text-fire-600">*</span></label>
<input id="t-start" type="datetime-local" required bind:value={terminZeit} class="w-full rounded-md border border-stein-300 px-3 py-2 text-sm focus:border-wald-500 focus:outline-none focus:ring-2 focus:ring-wald-200" />
</div>
<div>
<label class="mb-1 block text-sm font-medium text-stein-700" for="t-end">Ende <span class="text-stein-400">(optional)</span></label>
<input id="t-end" type="datetime-local" bind:value={terminEnde} class="w-full rounded-md border border-stein-300 px-3 py-2 text-sm focus:border-wald-500 focus:outline-none focus:ring-2 focus:ring-wald-200" />
</div>
<TextInput label="Beginn" type="datetime-local" required bind:value={terminZeit} size="sm" />
<TextInput label="Ende (optional)" type="datetime-local" bind:value={terminEnde} size="sm" />
</div>
<div>
<label class="mb-1 block text-sm font-medium text-stein-700" for="t-loc">Ort <span class="text-stein-400">(optional)</span></label>
<input id="t-loc" type="text" bind:value={locationText} placeholder="Mehrzweckhalle, 98553 …" class="w-full rounded-md border border-stein-300 px-3 py-2 text-sm focus:border-wald-500 focus:outline-none focus:ring-2 focus:ring-wald-200" />
</div>
<TextInput
label="Ort (optional)"
type="text"
bind:value={locationText}
placeholder="Mehrzweckhalle, 98553 …"
size="sm"
/>
<div>
<label class="mb-1 block text-sm font-medium text-stein-700" for="t-desc">Beschreibung <span class="text-stein-400">(optional)</span></label>
<textarea id="t-desc" rows="4" bind:value={description} placeholder="Worum geht es? Wer lädt ein?" class="w-full rounded-md border border-stein-300 px-3 py-2 text-sm focus:border-wald-500 focus:outline-none focus:ring-2 focus:ring-wald-200"></textarea>
</div>
<Textarea
label="Beschreibung (optional)"
rows={4}
bind:value={description}
placeholder="Worum geht es? Wer lädt ein?"
size="sm"
/>
<div>
<label class="mb-1 block text-sm font-medium text-stein-700" for="t-url">Quelle / Link <span class="text-stein-400">(optional)</span></label>
<input id="t-url" type="url" bind:value={sourceUrl} placeholder="https://…" class="w-full rounded-md border border-stein-300 px-3 py-2 text-sm focus:border-wald-500 focus:outline-none focus:ring-2 focus:ring-wald-200" />
</div>
<TextInput
label="Quelle / Link (optional)"
type="url"
bind:value={sourceUrl}
placeholder="https://…"
size="sm"
/>
{#if error}
<p class="text-sm text-fire-600">{error}</p>