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 {
|
||||
/* Shared card surface: border, background, shadow, radius */
|
||||
.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 */
|
||||
@@ -383,69 +383,14 @@ main ol:not(.not-prose):where(:not(.not-prose *)) li {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
/* Buttons – gemeinsame Basis */
|
||||
.btn-primary,
|
||||
.btn-secondary,
|
||||
.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;
|
||||
}
|
||||
/* .btn-*-Klassen entfernt (2026-07-12): Buttons laufen jetzt komplett über das
|
||||
Button-Atom (src/lib/ui/Button.svelte). Die --color-btn-* Tokens bleiben,
|
||||
weil das Atom `bg-btn-bg`/`text-btn-txt`/`bg-btn-hover-bg` nutzt. */
|
||||
|
||||
/* Filter-Chip — Basis für alle Filter-Chips (Kalender, Adressbuch, …).
|
||||
Aktiv/inaktiv-Farben werden kontextuell per Klasse gesetzt. */
|
||||
.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).
|
||||
|
||||
+18
-1
@@ -370,7 +370,7 @@ export interface OrganisationsBlockData {
|
||||
action?: string | { url?: string; linkName?: string; newTab?: boolean };
|
||||
description?: string;
|
||||
/** 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;
|
||||
addOrganisationMarkdown?: string;
|
||||
/** Referenz auf link-Collection (aufgelöst: url, linkName, newTab) */
|
||||
@@ -588,6 +588,8 @@ export interface InfoCardData {
|
||||
linkLabel?: string;
|
||||
linkHref?: string;
|
||||
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. */
|
||||
@@ -607,3 +609,18 @@ export interface SectionGridBlockData {
|
||||
cards?: (string | InfoCardData)[];
|
||||
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 OrganisationsMapBlock from "./blocks/OrganisationsMapBlock.svelte";
|
||||
import SectionGridBlock from "./blocks/SectionGridBlock.svelte";
|
||||
import ContactFormBlock from "./blocks/ContactFormBlock.svelte";
|
||||
import type { Translations } from "$lib/translations";
|
||||
import type {
|
||||
ResolvedBlock,
|
||||
@@ -53,6 +54,7 @@
|
||||
StellingnahmeGeneratorBlockData,
|
||||
OrganisationsMapBlockData,
|
||||
SectionGridBlockData,
|
||||
ContactFormBlockData,
|
||||
} from "$lib/block-types";
|
||||
|
||||
let {
|
||||
@@ -121,6 +123,8 @@
|
||||
<OrganisationsMapBlock block={block as OrganisationsMapBlockData} />
|
||||
{:else if type === "section_grid"}
|
||||
<SectionGridBlock block={block as SectionGridBlockData} />
|
||||
{:else if type === "contact_form"}
|
||||
<ContactFormBlock block={block as ContactFormBlockData} />
|
||||
{:else if type === "stellungnahme_generator"}
|
||||
{#await import("./blocks/StellingnahmeGeneratorBlock.svelte") then m}
|
||||
{@const Comp = m.default}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* tile + post: PostCard – Shell ohne Padding, Medien bündig, Text mit p-4 im Kind.
|
||||
* 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: {
|
||||
variant: {
|
||||
tile: "",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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
|
||||
* (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 "$lib/iconify-offline";
|
||||
import RustyImage from "$lib/components/RustyImage.svelte";
|
||||
import ArrowLink from "$lib/components/ArrowLink.svelte";
|
||||
|
||||
type Tone = "default" | "green";
|
||||
|
||||
let {
|
||||
icon = undefined,
|
||||
@@ -17,6 +22,8 @@
|
||||
linkLabel = undefined,
|
||||
linkHref = undefined,
|
||||
external = false,
|
||||
tone = "default",
|
||||
compact = false,
|
||||
}: {
|
||||
icon?: string;
|
||||
title: string;
|
||||
@@ -24,38 +31,65 @@
|
||||
linkLabel?: string;
|
||||
linkHref?: string;
|
||||
external?: boolean;
|
||||
tone?: Tone;
|
||||
compact?: boolean;
|
||||
} = $props();
|
||||
|
||||
// Bild-URL vs Iconify-String unterscheiden.
|
||||
const isImage = $derived(!!icon && (/^https?:\/\//.test(icon) || icon.startsWith("/")));
|
||||
const hasLink = $derived(!!linkLabel && !!linkHref);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-[14px] rounded-lg border border-[#d5d8d6] bg-white p-[22px]">
|
||||
{#if icon}
|
||||
<div class="flex size-11 shrink-0 items-center justify-center overflow-hidden rounded-full bg-wald-50 text-wald-500">
|
||||
{#if isImage}
|
||||
<RustyImage src={icon} width={44} aspect="1/1" alt="" class="size-full object-cover" />
|
||||
{:else}
|
||||
<Icon icon={icon} class="size-[23px]" aria-hidden="true" />
|
||||
{#snippet iconBadge(sizeClass: string, iconSize: string)}
|
||||
<div
|
||||
class="flex shrink-0 items-center justify-center overflow-hidden rounded-full {sizeClass} {isImage
|
||||
? 'border border-stein-200 bg-white'
|
||||
: tone === 'green'
|
||||
? 'bg-white text-wald-600'
|
||||
: '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}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h3 class="text-base font-bold tracking-[-0.01em] text-[#1f2221]">{title}</h3>
|
||||
{#if description}
|
||||
<p class="text-sm font-normal leading-[1.45] text-[#636966]">{description}</p>
|
||||
{/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 hasLink}
|
||||
<div class="mt-auto pt-0.5">
|
||||
<ArrowLink href={linkHref ?? "#"} label={linkLabel ?? ""} {external} size="sm" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<a
|
||||
{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">
|
||||
{#if rawImg}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<section>
|
||||
{#if hasHeader}
|
||||
<div class="mb-6">
|
||||
<div class="mb-4">
|
||||
<SectionHeader
|
||||
{number}
|
||||
{kicker}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* actionLabel="Zum Adressbuch" actionHref="/adressbuch" />
|
||||
*/
|
||||
import type { Snippet } from 'svelte';
|
||||
import ArrowLink from '$lib/components/ArrowLink.svelte';
|
||||
|
||||
let {
|
||||
number = undefined,
|
||||
@@ -69,13 +70,7 @@
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<a
|
||||
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>
|
||||
<ArrowLink href={actionHref ?? "#"} label={actionLabel ?? ""} size="md" />
|
||||
{/if}
|
||||
</div>
|
||||
{/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 Button from "$lib/ui/Button.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) {
|
||||
return extractCmsImageField(o.logo);
|
||||
@@ -32,6 +34,8 @@
|
||||
);
|
||||
const compact = $derived(block.presentation === "compact");
|
||||
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 ctaHtml = $derived(
|
||||
block.addOrganisationMarkdown
|
||||
@@ -166,7 +170,7 @@
|
||||
{/snippet}
|
||||
|
||||
<div class={layoutClasses} data-block="Organisations" data-block-type="organisations" data-block-slug={block._slug}>
|
||||
{#if block.headline}
|
||||
{#if block.headline && !grid}
|
||||
{#if compact}
|
||||
<h3 class="mb-2 text-lg font-semibold text-stein-900">{block.headline}</h3>
|
||||
{:else}
|
||||
@@ -183,7 +187,29 @@
|
||||
{/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
|
||||
bind:this={scroller}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
linkLabel={card.linkLabel}
|
||||
linkHref={card.linkHref}
|
||||
external={card.external}
|
||||
tone={card.tone}
|
||||
/>
|
||||
{/each}
|
||||
</SectionGrid>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { env as publicEnv } from "$env/dynamic/public";
|
||||
import Button from "$lib/ui/Button.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
import "$lib/iconify-offline";
|
||||
import TextInput from "$lib/ui/TextInput.svelte";
|
||||
import Textarea from "$lib/ui/Textarea.svelte";
|
||||
import Checkbox from "$lib/ui/Checkbox.svelte";
|
||||
@@ -291,11 +293,15 @@
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
size="md"
|
||||
loading={formState === "pending"}
|
||||
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}
|
||||
<span class="text-xs text-error">{t(T.contact_fix_errors)}</span>
|
||||
{/if}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
const sizeClasses = $derived(
|
||||
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'
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
const sizeClasses = $derived(
|
||||
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'
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -399,7 +399,11 @@
|
||||
<Breadcrumbs items={breadcrumbItems} />
|
||||
</div>
|
||||
{/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>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user