refactor(consolidate): IconButton + Badge-solid + @layer base + Massen-Migration
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:
@@ -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 h1–h6 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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 } };
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user