feat(blocks): SectionGrid/InfoCard/ContactForm + Konsolidierung (Tier 1)
Neue Komponenten: SectionGrid, InfoCard (tone/compact), ArrowLink, ContactFormBlock, SectionGridBlock. CMS-Blöcke section_grid + contact_form (info_card-Collection). OrganisationsBlock: presentation "grid" (SectionGrid + InfoCard-Tiles). Kontaktformular-Submit: size md + Send-Icon. Konsolidierung Tier 1: - .card-surface + Card-Atom → rounded-lg, .chip → rounded-full, Input-Atome → rounded-md - tote .btn-*-Klassen aus app.css entfernt (Atom nutzt --color-btn-*-Tokens) - hardcoded Hex → stein-Tokens (InfoCard, ContactFormBlock) - redundante rounded-sm auf card-surface-Nutzern entfernt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-60
@@ -17,7 +17,7 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
/* Shared card surface: border, background, shadow, radius */
|
/* Shared card surface: border, background, shadow, radius */
|
||||||
.card-surface {
|
.card-surface {
|
||||||
@apply rounded-sm border border-stein-200 bg-white shadow-sm;
|
@apply rounded-lg border border-stein-200 bg-white shadow-sm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Diagonal strikethrough for past calendar days */
|
/* Diagonal strikethrough for past calendar days */
|
||||||
@@ -383,69 +383,14 @@ main ol:not(.not-prose):where(:not(.not-prose *)) li {
|
|||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons – gemeinsame Basis */
|
/* .btn-*-Klassen entfernt (2026-07-12): Buttons laufen jetzt komplett über das
|
||||||
.btn-primary,
|
Button-Atom (src/lib/ui/Button.svelte). Die --color-btn-* Tokens bleiben,
|
||||||
.btn-secondary,
|
weil das Atom `bg-btn-bg`/`text-btn-txt`/`bg-btn-hover-bg` nutzt. */
|
||||||
.btn-tertiary,
|
|
||||||
.btn-outline,
|
|
||||||
.btn-warning {
|
|
||||||
@apply inline-flex items-center gap-1 no-underline whitespace-nowrap transition-colors duration-200 font-medium px-4 py-2 rounded-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons – Wald (Primary) */
|
|
||||||
.btn-primary {
|
|
||||||
@apply shadow-sm bg-btn-bg text-btn-txt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
@apply bg-btn-hover-bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons – Stein outline (Secondary) */
|
|
||||||
.btn-secondary {
|
|
||||||
@apply border border-stein-300 bg-white text-stein-700 shadow-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
@apply bg-stein-100 border-stein-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons – Himmel (Tertiary) */
|
|
||||||
.btn-tertiary {
|
|
||||||
@apply shadow-sm bg-himmel-700 text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-tertiary:hover {
|
|
||||||
@apply bg-himmel-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons – Stein-Border klein (Outline) */
|
|
||||||
.btn-outline {
|
|
||||||
@apply border border-stein-300 text-stein-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover {
|
|
||||||
@apply bg-stein-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons – Gelb (Signal / Aufmerksamkeit, z. B. Termin melden) */
|
|
||||||
.btn-warning {
|
|
||||||
@apply border border-gelb-500 bg-gelb-300 text-gelb-900 shadow-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning:hover {
|
|
||||||
@apply bg-gelb-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Größen-Modifier — nach den btn-* definiert damit override greift */
|
|
||||||
.btn-sm {
|
|
||||||
@apply px-2 py-1 text-xs gap-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter-Chip — Basis für alle Filter-Chips (Kalender, Adressbuch, …).
|
/* Filter-Chip — Basis für alle Filter-Chips (Kalender, Adressbuch, …).
|
||||||
Aktiv/inaktiv-Farben werden kontextuell per Klasse gesetzt. */
|
Aktiv/inaktiv-Farben werden kontextuell per Klasse gesetzt. */
|
||||||
.chip {
|
.chip {
|
||||||
@apply inline-flex cursor-pointer items-center whitespace-nowrap rounded-xs border px-2 py-0.5 text-[11px] font-medium transition-colors;
|
@apply inline-flex cursor-pointer items-center whitespace-nowrap rounded-full border px-2 py-0.5 text-[11px] font-medium transition-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dekorativer Hintergrund mit zwei verschwommenen Verlaufs-Blobs (Wald → Erde).
|
/* Dekorativer Hintergrund mit zwei verschwommenen Verlaufs-Blobs (Wald → Erde).
|
||||||
|
|||||||
+18
-1
@@ -370,7 +370,7 @@ export interface OrganisationsBlockData {
|
|||||||
action?: string | { url?: string; linkName?: string; newTab?: boolean };
|
action?: string | { url?: string; linkName?: string; newTab?: boolean };
|
||||||
description?: string;
|
description?: string;
|
||||||
/** default = Karten-Grid; compact = engere Karten, Logo links; carousel = horizontaler Slider (3 Desktop / 2 Mobile) */
|
/** default = Karten-Grid; compact = engere Karten, Logo links; carousel = horizontaler Slider (3 Desktop / 2 Mobile) */
|
||||||
presentation?: "default" | "compact" | "carousel";
|
presentation?: "default" | "compact" | "carousel" | "grid";
|
||||||
addOrganisationTitle?: string;
|
addOrganisationTitle?: string;
|
||||||
addOrganisationMarkdown?: string;
|
addOrganisationMarkdown?: string;
|
||||||
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
||||||
@@ -588,6 +588,8 @@ export interface InfoCardData {
|
|||||||
linkLabel?: string;
|
linkLabel?: string;
|
||||||
linkHref?: string;
|
linkHref?: string;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
|
/** Farbton der Kachel: default (weiß) oder green (grün getönt). */
|
||||||
|
tone?: "default" | "green";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sektions-Grid (_type: "section_grid"): SectionHeader + info_card-Kacheln. */
|
/** Sektions-Grid (_type: "section_grid"): SectionHeader + info_card-Kacheln. */
|
||||||
@@ -607,3 +609,18 @@ export interface SectionGridBlockData {
|
|||||||
cards?: (string | InfoCardData)[];
|
cards?: (string | InfoCardData)[];
|
||||||
layout?: BlockLayout;
|
layout?: BlockLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Zweispaltiger Formular-Block (_type: "contact_form"): Form links, Aside rechts. */
|
||||||
|
export interface ContactFormBlockData {
|
||||||
|
_type?: "contact_form";
|
||||||
|
_slug?: string;
|
||||||
|
headline?: string;
|
||||||
|
kicker?: string;
|
||||||
|
number?: string;
|
||||||
|
action?: string | { url?: string; linkName?: string; newTab?: boolean };
|
||||||
|
form?: "form-contact" | "form-mitmachen" | "form-newsletter" | "form-newsletter-inline";
|
||||||
|
asideTitle?: string;
|
||||||
|
asideText?: string;
|
||||||
|
asideCards?: (string | InfoCardData)[];
|
||||||
|
layout?: BlockLayout;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/sveltekit';
|
||||||
|
import ArrowLink from './ArrowLink.svelte';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Molecules/ArrowLink',
|
||||||
|
component: ArrowLink,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: { layout: 'centered' },
|
||||||
|
args: { href: '#', label: 'Mehr erfahren' },
|
||||||
|
argTypes: {
|
||||||
|
size: { control: 'select', options: ['sm', 'md'] },
|
||||||
|
external: { control: 'boolean' },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof ArrowLink>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Medium: Story = { args: { size: 'md', label: 'Zum Adressbuch' } };
|
||||||
|
export const Klein: Story = { args: { size: 'sm', label: 'cloud.windwiderstand.de' } };
|
||||||
|
export const Extern: Story = { args: { size: 'sm', label: 'Beitreten', external: true } };
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* ArrowLink — Text-Link mit Pfeil, himmel-Farbe. Die eine Quelle für die
|
||||||
|
* „… →"-Links (SectionHeader-CTA, InfoCard, Sektions-Aktionen).
|
||||||
|
* `size`: sm (kompakt, z.B. in Cards) · md (Sektionsköpfe).
|
||||||
|
*/
|
||||||
|
type Size = "sm" | "md";
|
||||||
|
|
||||||
|
let {
|
||||||
|
href,
|
||||||
|
label,
|
||||||
|
external = false,
|
||||||
|
size = "md",
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
external?: boolean;
|
||||||
|
size?: Size;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const textClass: Record<Size, string> = {
|
||||||
|
sm: "text-[13px]",
|
||||||
|
md: "text-[15px]",
|
||||||
|
};
|
||||||
|
const arrowClass: Record<Size, string> = {
|
||||||
|
sm: "size-3.5",
|
||||||
|
md: "size-4",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
{href}
|
||||||
|
target={external ? "_blank" : undefined}
|
||||||
|
rel={external ? "noopener noreferrer" : undefined}
|
||||||
|
class="inline-flex items-center gap-1.5 font-semibold text-himmel-500 no-underline hover:text-himmel-600 {textClass[size]}"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<svg class={arrowClass[size]} viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</a>
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
import AdressbuchBlock from "./blocks/AdressbuchBlock.svelte";
|
import AdressbuchBlock from "./blocks/AdressbuchBlock.svelte";
|
||||||
import OrganisationsMapBlock from "./blocks/OrganisationsMapBlock.svelte";
|
import OrganisationsMapBlock from "./blocks/OrganisationsMapBlock.svelte";
|
||||||
import SectionGridBlock from "./blocks/SectionGridBlock.svelte";
|
import SectionGridBlock from "./blocks/SectionGridBlock.svelte";
|
||||||
|
import ContactFormBlock from "./blocks/ContactFormBlock.svelte";
|
||||||
import type { Translations } from "$lib/translations";
|
import type { Translations } from "$lib/translations";
|
||||||
import type {
|
import type {
|
||||||
ResolvedBlock,
|
ResolvedBlock,
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
StellingnahmeGeneratorBlockData,
|
StellingnahmeGeneratorBlockData,
|
||||||
OrganisationsMapBlockData,
|
OrganisationsMapBlockData,
|
||||||
SectionGridBlockData,
|
SectionGridBlockData,
|
||||||
|
ContactFormBlockData,
|
||||||
} from "$lib/block-types";
|
} from "$lib/block-types";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -121,6 +123,8 @@
|
|||||||
<OrganisationsMapBlock block={block as OrganisationsMapBlockData} />
|
<OrganisationsMapBlock block={block as OrganisationsMapBlockData} />
|
||||||
{:else if type === "section_grid"}
|
{:else if type === "section_grid"}
|
||||||
<SectionGridBlock block={block as SectionGridBlockData} />
|
<SectionGridBlock block={block as SectionGridBlockData} />
|
||||||
|
{:else if type === "contact_form"}
|
||||||
|
<ContactFormBlock block={block as ContactFormBlockData} />
|
||||||
{:else if type === "stellungnahme_generator"}
|
{:else if type === "stellungnahme_generator"}
|
||||||
{#await import("./blocks/StellingnahmeGeneratorBlock.svelte") then m}
|
{#await import("./blocks/StellingnahmeGeneratorBlock.svelte") then m}
|
||||||
{@const Comp = m.default}
|
{@const Comp = m.default}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* tile + post: PostCard – Shell ohne Padding, Medien bündig, Text mit p-4 im Kind.
|
* tile + post: PostCard – Shell ohne Padding, Medien bündig, Text mit p-4 im Kind.
|
||||||
* callout: CTA; layout wird ignoriert.
|
* callout: CTA; layout wird ignoriert.
|
||||||
*/
|
*/
|
||||||
const cardSurface = cva("relative min-w-0 overflow-hidden rounded-sm", {
|
const cardSurface = cva("relative min-w-0 overflow-hidden rounded-lg", {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
tile: "",
|
tile: "",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="not-prose card-surface relative flex h-full min-w-0 flex-col gap-2 overflow-hidden rounded-sm p-4 transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:border-wald-300 hover:shadow-md"
|
class="not-prose card-surface relative flex h-full min-w-0 flex-col gap-2 overflow-hidden p-4 transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:border-wald-300 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<p class="truncate pr-6 text-sm font-medium leading-tight text-stein-900">{contact.name ?? ""}</p>
|
<p class="truncate pr-6 text-sm font-medium leading-tight text-stein-900">{contact.name ?? ""}</p>
|
||||||
|
|||||||
@@ -5,10 +5,15 @@
|
|||||||
*
|
*
|
||||||
* `icon` ist entweder ein Iconify-String ("mdi:email") ODER eine Bild-URL
|
* `icon` ist entweder ein Iconify-String ("mdi:email") ODER eine Bild-URL
|
||||||
* (https://… oder /…) — wird automatisch erkannt.
|
* (https://… oder /…) — wird automatisch erkannt.
|
||||||
|
* `tone`: default (weiß) oder green (grün getönt, z.B. Aside-Boxen).
|
||||||
|
* `compact`: kleinere Maße + Icon inline neben dem Titel (spart Höhe).
|
||||||
*/
|
*/
|
||||||
import Icon from "@iconify/svelte";
|
import Icon from "@iconify/svelte";
|
||||||
import "$lib/iconify-offline";
|
import "$lib/iconify-offline";
|
||||||
import RustyImage from "$lib/components/RustyImage.svelte";
|
import RustyImage from "$lib/components/RustyImage.svelte";
|
||||||
|
import ArrowLink from "$lib/components/ArrowLink.svelte";
|
||||||
|
|
||||||
|
type Tone = "default" | "green";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
icon = undefined,
|
icon = undefined,
|
||||||
@@ -17,6 +22,8 @@
|
|||||||
linkLabel = undefined,
|
linkLabel = undefined,
|
||||||
linkHref = undefined,
|
linkHref = undefined,
|
||||||
external = false,
|
external = false,
|
||||||
|
tone = "default",
|
||||||
|
compact = false,
|
||||||
}: {
|
}: {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -24,38 +31,65 @@
|
|||||||
linkLabel?: string;
|
linkLabel?: string;
|
||||||
linkHref?: string;
|
linkHref?: string;
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
|
tone?: Tone;
|
||||||
|
compact?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// Bild-URL vs Iconify-String unterscheiden.
|
|
||||||
const isImage = $derived(!!icon && (/^https?:\/\//.test(icon) || icon.startsWith("/")));
|
const isImage = $derived(!!icon && (/^https?:\/\//.test(icon) || icon.startsWith("/")));
|
||||||
const hasLink = $derived(!!linkLabel && !!linkHref);
|
const hasLink = $derived(!!linkLabel && !!linkHref);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col gap-[14px] rounded-lg border border-[#d5d8d6] bg-white p-[22px]">
|
{#snippet iconBadge(sizeClass: string, iconSize: string)}
|
||||||
{#if icon}
|
<div
|
||||||
<div class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-full bg-wald-50 text-wald-500">
|
class="flex shrink-0 items-center justify-center overflow-hidden rounded-full {sizeClass} {isImage
|
||||||
{#if isImage}
|
? 'border border-stein-200 bg-white'
|
||||||
<RustyImage src={icon} width={44} aspect="1/1" alt="" class="size-full object-cover" />
|
: tone === 'green'
|
||||||
{:else}
|
? 'bg-white text-wald-600'
|
||||||
<Icon icon={icon} class="size-[23px]" aria-hidden="true" />
|
: 'bg-wald-50 text-wald-500'}"
|
||||||
|
>
|
||||||
|
{#if isImage}
|
||||||
|
<RustyImage src={icon ?? ""} width={44} aspect="1/1" fit="contain" alt="" class="size-full object-contain p-0.5" />
|
||||||
|
{:else}
|
||||||
|
<Icon icon={icon ?? ""} class={iconSize} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex h-full flex-col rounded-lg border {compact ? 'gap-2 p-4' : 'gap-[14px] p-[22px]'} {tone ===
|
||||||
|
'green'
|
||||||
|
? 'border-wald-100 bg-wald-50'
|
||||||
|
: 'border-stein-200 bg-white'}"
|
||||||
|
>
|
||||||
|
{#if compact}
|
||||||
|
<!-- Kompakt: Icon hängt links, Text-Spalte (Titel/Desc/Link) rechts bündig. -->
|
||||||
|
<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>
|
||||||
|
{#if description}
|
||||||
|
<p class="line-clamp-3 text-[13px] font-normal leading-[1.45] text-stein-500">{description}</p>
|
||||||
|
{/if}
|
||||||
|
{#if hasLink}
|
||||||
|
<div class="pt-0.5">
|
||||||
|
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{: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>
|
||||||
|
{#if description}
|
||||||
|
<p class="line-clamp-3 text-sm font-normal leading-[1.45] text-stein-500">{description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{#if hasLink}
|
||||||
|
<div class="mt-auto pt-0.5">
|
||||||
<h3 class="text-base font-bold tracking-[-0.01em] text-[#1f2221]">{title}</h3>
|
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
|
||||||
{#if description}
|
</div>
|
||||||
<p class="text-sm font-normal leading-[1.45] text-[#636966]">{description}</p>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if hasLink}
|
|
||||||
<a
|
|
||||||
href={linkHref}
|
|
||||||
target={external ? "_blank" : undefined}
|
|
||||||
rel={external ? "noopener noreferrer" : undefined}
|
|
||||||
class="mt-auto inline-flex items-center gap-1.5 text-sm font-semibold text-himmel-500 no-underline hover:text-himmel-600"
|
|
||||||
>
|
|
||||||
{linkLabel}
|
|
||||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
{href}
|
{href}
|
||||||
class="group flex gap-3 overflow-hidden rounded-sm card-surface p-0 text-stein-900 no-underline transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-wald-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500 focus-visible:ring-offset-2"
|
class="group flex gap-3 overflow-hidden card-surface p-0 text-stein-900 no-underline transition-[transform,box-shadow,border-color] duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-wald-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-wald-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<div class="relative w-28 shrink-0 self-stretch overflow-hidden bg-stein-100 sm:w-32">
|
<div class="relative w-28 shrink-0 self-stretch overflow-hidden bg-stein-100 sm:w-32">
|
||||||
{#if rawImg}
|
{#if rawImg}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
{#if hasHeader}
|
{#if hasHeader}
|
||||||
<div class="mb-6">
|
<div class="mb-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
{number}
|
{number}
|
||||||
{kicker}
|
{kicker}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* actionLabel="Zum Adressbuch" actionHref="/adressbuch" />
|
* actionLabel="Zum Adressbuch" actionHref="/adressbuch" />
|
||||||
*/
|
*/
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import ArrowLink from '$lib/components/ArrowLink.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
number = undefined,
|
number = undefined,
|
||||||
@@ -69,13 +70,7 @@
|
|||||||
{#if children}
|
{#if children}
|
||||||
{@render children()}
|
{@render children()}
|
||||||
{:else}
|
{:else}
|
||||||
<a
|
<ArrowLink href={actionHref ?? "#"} label={actionLabel ?? ""} size="md" />
|
||||||
href={actionHref}
|
|
||||||
class="inline-flex items-center gap-1.5 text-[15px] font-semibold text-himmel-500 no-underline hover:text-himmel-600"
|
|
||||||
>
|
|
||||||
{actionLabel}
|
|
||||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { marked } from "$lib/markdown-safe";
|
||||||
|
import { getBlockLayoutClasses } from "$lib/block-layout";
|
||||||
|
import SectionHeader from "$lib/components/SectionHeader.svelte";
|
||||||
|
import InfoCard from "$lib/components/InfoCard.svelte";
|
||||||
|
import InternalComponentBlock from "./InternalComponentBlock.svelte";
|
||||||
|
import type { ContactFormBlockData, InfoCardData } from "$lib/block-types";
|
||||||
|
|
||||||
|
let { block }: { block: ContactFormBlockData } = $props();
|
||||||
|
|
||||||
|
const layoutClasses = $derived(getBlockLayoutClasses(block.layout));
|
||||||
|
const actionRef = $derived(
|
||||||
|
block.action && typeof block.action === "object" ? block.action : null,
|
||||||
|
);
|
||||||
|
const hasHeader = $derived(!!block.number || !!block.kicker || !!block.headline);
|
||||||
|
const asideHtml = $derived(
|
||||||
|
block.asideText && typeof block.asideText === "string"
|
||||||
|
? (marked.parse(block.asideText) as string)
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
const cards = $derived(
|
||||||
|
(block.asideCards ?? []).filter(
|
||||||
|
(c): c is InfoCardData => typeof c === "object" && c !== null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const formId = $derived(block.form ?? "form-contact");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={layoutClasses}
|
||||||
|
data-block="ContactForm"
|
||||||
|
data-block-type="contact_form"
|
||||||
|
data-block-slug={block._slug}
|
||||||
|
>
|
||||||
|
{#if hasHeader}
|
||||||
|
<div class="mb-4">
|
||||||
|
<SectionHeader
|
||||||
|
number={block.number}
|
||||||
|
kicker={block.kicker}
|
||||||
|
title={block.headline}
|
||||||
|
actionLabel={actionRef?.linkName}
|
||||||
|
actionHref={actionRef?.url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-8 lg:grid-cols-[1.4fr_1fr] lg:items-start">
|
||||||
|
<!-- links: Formular -->
|
||||||
|
<div>
|
||||||
|
<InternalComponentBlock block={{ _type: "internal_component", component: formId }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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">
|
||||||
|
{block.asideTitle}
|
||||||
|
</h3>
|
||||||
|
{/if}
|
||||||
|
{#if asideHtml}
|
||||||
|
<div class="markdown prose prose-stein max-w-none text-stein-500">{@html asideHtml}</div>
|
||||||
|
{/if}
|
||||||
|
{#each cards as card}
|
||||||
|
<InfoCard
|
||||||
|
icon={card.icon}
|
||||||
|
title={card.title ?? ""}
|
||||||
|
description={card.description}
|
||||||
|
linkLabel={card.linkLabel}
|
||||||
|
linkHref={card.linkHref}
|
||||||
|
external={card.external}
|
||||||
|
tone={card.tone}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
import RustyImage from "$lib/components/RustyImage.svelte";
|
import RustyImage from "$lib/components/RustyImage.svelte";
|
||||||
import Button from "$lib/ui/Button.svelte";
|
import Button from "$lib/ui/Button.svelte";
|
||||||
import SectionHeader from "$lib/components/SectionHeader.svelte";
|
import SectionHeader from "$lib/components/SectionHeader.svelte";
|
||||||
|
import SectionGrid from "$lib/components/SectionGrid.svelte";
|
||||||
|
import InfoCard from "$lib/components/InfoCard.svelte";
|
||||||
|
|
||||||
function organisationLogoField(o: OrganisationEntry) {
|
function organisationLogoField(o: OrganisationEntry) {
|
||||||
return extractCmsImageField(o.logo);
|
return extractCmsImageField(o.logo);
|
||||||
@@ -32,6 +34,8 @@
|
|||||||
);
|
);
|
||||||
const compact = $derived(block.presentation === "compact");
|
const compact = $derived(block.presentation === "compact");
|
||||||
const carousel = $derived(block.presentation === "carousel");
|
const carousel = $derived(block.presentation === "carousel");
|
||||||
|
// "grid" = SectionGrid + InfoCard-Tiles (Logo im Kreis, Name, Beschreibung).
|
||||||
|
const grid = $derived(block.presentation === "grid");
|
||||||
const orgs = $derived(block.organisations ?? []);
|
const orgs = $derived(block.organisations ?? []);
|
||||||
const ctaHtml = $derived(
|
const ctaHtml = $derived(
|
||||||
block.addOrganisationMarkdown
|
block.addOrganisationMarkdown
|
||||||
@@ -166,7 +170,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class={layoutClasses} data-block="Organisations" data-block-type="organisations" data-block-slug={block._slug}>
|
<div class={layoutClasses} data-block="Organisations" data-block-type="organisations" data-block-slug={block._slug}>
|
||||||
{#if block.headline}
|
{#if block.headline && !grid}
|
||||||
{#if compact}
|
{#if compact}
|
||||||
<h3 class="mb-2 text-lg font-semibold text-stein-900">{block.headline}</h3>
|
<h3 class="mb-2 text-lg font-semibold text-stein-900">{block.headline}</h3>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -183,7 +187,29 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if carousel}
|
{#if grid}
|
||||||
|
<SectionGrid
|
||||||
|
number={block.number}
|
||||||
|
kicker={block.kicker}
|
||||||
|
title={block.headline}
|
||||||
|
subtitle={block.description}
|
||||||
|
actionLabel={actionRef?.linkName}
|
||||||
|
actionHref={actionRef?.url}
|
||||||
|
columns={3}
|
||||||
|
>
|
||||||
|
{#each orgs as org}
|
||||||
|
{#if typeof org === "object" && org !== null}
|
||||||
|
{@const o = org as OrganisationEntry}
|
||||||
|
{@const logoField = organisationLogoField(o)}
|
||||||
|
<InfoCard
|
||||||
|
icon={logoField?.url ?? "mdi:account-group"}
|
||||||
|
title={o.name ?? ""}
|
||||||
|
description={o.description}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</SectionGrid>
|
||||||
|
{:else if carousel}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
bind:this={scroller}
|
bind:this={scroller}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
linkLabel={card.linkLabel}
|
linkLabel={card.linkLabel}
|
||||||
linkHref={card.linkHref}
|
linkHref={card.linkHref}
|
||||||
external={card.external}
|
external={card.external}
|
||||||
|
tone={card.tone}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</SectionGrid>
|
</SectionGrid>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { env as publicEnv } from "$env/dynamic/public";
|
import { env as publicEnv } from "$env/dynamic/public";
|
||||||
import Button from "$lib/ui/Button.svelte";
|
import Button from "$lib/ui/Button.svelte";
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import "$lib/iconify-offline";
|
||||||
import TextInput from "$lib/ui/TextInput.svelte";
|
import TextInput from "$lib/ui/TextInput.svelte";
|
||||||
import Textarea from "$lib/ui/Textarea.svelte";
|
import Textarea from "$lib/ui/Textarea.svelte";
|
||||||
import Checkbox from "$lib/ui/Checkbox.svelte";
|
import Checkbox from "$lib/ui/Checkbox.svelte";
|
||||||
@@ -291,11 +293,15 @@
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="md"
|
||||||
loading={formState === "pending"}
|
loading={formState === "pending"}
|
||||||
disabled={!isValid && submitAttempted}
|
disabled={!isValid && submitAttempted}
|
||||||
label={formState === "pending" ? t(T.contact_submitting) : t(T.contact_submit)}
|
>
|
||||||
/>
|
{#if formState !== "pending"}
|
||||||
|
<Icon icon="lucide:send" class="size-4" aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
{formState === "pending" ? t(T.contact_submitting) : t(T.contact_submit)}
|
||||||
|
</Button>
|
||||||
{#if !isValid && submitAttempted && !serverError}
|
{#if !isValid && submitAttempted && !serverError}
|
||||||
<span class="text-xs text-error">{t(T.contact_fix_errors)}</span>
|
<span class="text-xs text-error">{t(T.contact_fix_errors)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const sizeClasses = $derived(
|
const sizeClasses = $derived(
|
||||||
size === 'sm'
|
size === 'sm'
|
||||||
? 'h-9 rounded-sm px-2.5 py-1.5 text-xs shadow-sm'
|
? 'h-9 rounded-md px-2.5 py-1.5 text-xs shadow-sm'
|
||||||
: 'h-12 rounded-lg px-4 py-3 text-base'
|
: 'h-12 rounded-lg px-4 py-3 text-base'
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
const sizeClasses = $derived(
|
const sizeClasses = $derived(
|
||||||
size === 'sm'
|
size === 'sm'
|
||||||
? 'min-h-[80px] rounded-sm px-2.5 py-1.5 text-xs shadow-sm'
|
? 'min-h-[80px] rounded-md px-2.5 py-1.5 text-xs shadow-sm'
|
||||||
: 'min-h-[120px] rounded-lg px-4 py-3 text-base'
|
: 'min-h-[120px] rounded-lg px-4 py-3 text-base'
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -399,7 +399,11 @@
|
|||||||
<Breadcrumbs items={breadcrumbItems} />
|
<Breadcrumbs items={breadcrumbItems} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{@render children()}
|
<!-- Konsolidierter Top-Abstand vorm ersten Content-Element (alle
|
||||||
|
Seitentypen), zusätzlich zum Breadcrumb-py. -->
|
||||||
|
<div class="pt-4 md:pt-6">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user