From f77314dc4939ff06ed9797b9a73e2bcd5286f870 Mon Sep 17 00:00:00 2001 From: Peter Meier Date: Sun, 12 Jul 2026 10:49:54 +0200 Subject: [PATCH] feat(blocks): SectionGrid/InfoCard/ContactForm + Konsolidierung (Tier 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/app.css | 65 ++------------ src/lib/block-types.ts | 19 ++++- src/lib/components/ArrowLink.stories.ts | 21 +++++ src/lib/components/ArrowLink.svelte | 39 +++++++++ src/lib/components/BlockRenderer.svelte | 4 + src/lib/components/Card.svelte | 2 +- src/lib/components/ContactCard.svelte | 2 +- src/lib/components/InfoCard.svelte | 84 +++++++++++++------ src/lib/components/PostCardCompact.svelte | 2 +- src/lib/components/SectionGrid.svelte | 2 +- src/lib/components/SectionHeader.svelte | 9 +- .../components/blocks/ContactFormBlock.svelte | 77 +++++++++++++++++ .../blocks/OrganisationsBlock.svelte | 30 ++++++- .../components/blocks/SectionGridBlock.svelte | 1 + .../internal/ContactFormComponent.svelte | 12 ++- src/lib/ui/TextInput.svelte | 2 +- src/lib/ui/Textarea.svelte | 2 +- src/routes/+layout.svelte | 6 +- 18 files changed, 274 insertions(+), 105 deletions(-) create mode 100644 src/lib/components/ArrowLink.stories.ts create mode 100644 src/lib/components/ArrowLink.svelte create mode 100644 src/lib/components/blocks/ContactFormBlock.svelte diff --git a/src/app.css b/src/app.css index 6cad1d1..fd09552 100644 --- a/src/app.css +++ b/src/app.css @@ -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). diff --git a/src/lib/block-types.ts b/src/lib/block-types.ts index e064fde..73a1db0 100644 --- a/src/lib/block-types.ts +++ b/src/lib/block-types.ts @@ -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; +} diff --git a/src/lib/components/ArrowLink.stories.ts b/src/lib/components/ArrowLink.stories.ts new file mode 100644 index 0000000..843c854 --- /dev/null +++ b/src/lib/components/ArrowLink.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +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 } }; diff --git a/src/lib/components/ArrowLink.svelte b/src/lib/components/ArrowLink.svelte new file mode 100644 index 0000000..22acfc0 --- /dev/null +++ b/src/lib/components/ArrowLink.svelte @@ -0,0 +1,39 @@ + + + + {label} + + diff --git a/src/lib/components/BlockRenderer.svelte b/src/lib/components/BlockRenderer.svelte index 31fcf15..c3200e2 100644 --- a/src/lib/components/BlockRenderer.svelte +++ b/src/lib/components/BlockRenderer.svelte @@ -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 @@ {:else if type === "section_grid"} +{:else if type === "contact_form"} + {:else if type === "stellungnahme_generator"} {#await import("./blocks/StellingnahmeGeneratorBlock.svelte") then m} {@const Comp = m.default} diff --git a/src/lib/components/Card.svelte b/src/lib/components/Card.svelte index 49d56e9..707aef3 100644 --- a/src/lib/components/Card.svelte +++ b/src/lib/components/Card.svelte @@ -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: "", diff --git a/src/lib/components/ContactCard.svelte b/src/lib/components/ContactCard.svelte index d75a364..2cbc5b2 100644 --- a/src/lib/components/ContactCard.svelte +++ b/src/lib/components/ContactCard.svelte @@ -16,7 +16,7 @@

{contact.name ?? ""}

diff --git a/src/lib/components/InfoCard.svelte b/src/lib/components/InfoCard.svelte index a523bc1..30d3b99 100644 --- a/src/lib/components/InfoCard.svelte +++ b/src/lib/components/InfoCard.svelte @@ -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); -
- {#if icon} -
- {#if isImage} - - {:else} -