feat: add class-variance-authority for card styling and enhance color palettes
- Introduced `class-variance-authority` to manage card component styles, allowing for flexible variant and layout options. - Added a new color palette for "Fire" to the CSS for improved theming. - Updated `Card` and `PostCard` components to utilize the new styling system, enhancing visual consistency and responsiveness. - Refined layout and styling in various components for better user experience.
This commit is contained in:
+13
@@ -51,6 +51,19 @@
|
||||
--color-himmel-800: #1a2c36;
|
||||
--color-himmel-900: #0e181e;
|
||||
|
||||
/* Palette: Fire (Tertiary) */
|
||||
--color-fire-50: #fdf2f1;
|
||||
--color-fire-100: #fbe5e3;
|
||||
--color-fire-200: #f7c9c5;
|
||||
--color-fire-300: #f2a8a2;
|
||||
--color-fire-400: #eb8078;
|
||||
--color-fire-500: #e35651;
|
||||
--color-fire-600: #d93d36;
|
||||
--color-fire-700: #c82e27;
|
||||
--color-fire-800: #b5231e;
|
||||
--color-fire-900: #981b18;
|
||||
--color-fire-950: #5d0f0b;
|
||||
|
||||
/* Palette: Stein (Neutrals) */
|
||||
--color-stein-0: #ffffff;
|
||||
--color-stein-50: #f7f8f7;
|
||||
|
||||
@@ -1,11 +1,68 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
/**
|
||||
* tile + stack/inline: Org-Karten (Padding im Shell).
|
||||
* 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", {
|
||||
variants: {
|
||||
variant: {
|
||||
tile: "",
|
||||
callout:
|
||||
"flex flex-col gap-1.5 border border-dashed border-wald-300 bg-wald-50 p-3",
|
||||
},
|
||||
layout: {
|
||||
stack: "",
|
||||
inline: "",
|
||||
post: "",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: "tile",
|
||||
layout: "stack",
|
||||
class:
|
||||
"flex flex-col gap-2 border border-gray-200 bg-white p-4 text-slate-950",
|
||||
},
|
||||
{
|
||||
variant: "tile",
|
||||
layout: "inline",
|
||||
class:
|
||||
"flex flex-row items-start gap-3 border border-gray-200 bg-white p-4 text-slate-950",
|
||||
},
|
||||
{
|
||||
variant: "tile",
|
||||
layout: "post",
|
||||
class:
|
||||
"flex flex-col gap-0 border border-gray-200 bg-white p-0 text-slate-950",
|
||||
},
|
||||
],
|
||||
defaultVariants: { variant: "tile", layout: "stack" },
|
||||
});
|
||||
|
||||
const cardAsLink = cva("", {
|
||||
variants: {
|
||||
variant: {
|
||||
callout: "cursor-pointer transition-all no-underline text-inherit",
|
||||
tile: "transition-all no-underline hover:border-gray-300",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "tile" },
|
||||
});
|
||||
|
||||
type SurfaceProps = VariantProps<typeof cardSurface>;
|
||||
type CardLinkVariant = NonNullable<SurfaceProps["variant"]>;
|
||||
|
||||
let {
|
||||
children,
|
||||
href,
|
||||
target,
|
||||
rel,
|
||||
variant = "tile",
|
||||
layout = "stack",
|
||||
class: className = "",
|
||||
}: {
|
||||
children?: Snippet;
|
||||
@@ -13,20 +70,20 @@
|
||||
target?: string;
|
||||
rel?: string;
|
||||
class?: string;
|
||||
} = $props();
|
||||
} & SurfaceProps = $props();
|
||||
|
||||
const base = "relative flex flex-col gap-2 rounded-lg border border-stein-200 bg-white p-4 shadow-sm min-w-0 overflow-hidden";
|
||||
const interactive = $derived(
|
||||
href ? "hover:border-stein-300 hover:shadow-md transition-all no-underline text-inherit" : "",
|
||||
const surface = $derived(cardSurface({ variant, layout }));
|
||||
const linkSurface = $derived(
|
||||
href ? cardAsLink({ variant: (variant ?? "tile") as CardLinkVariant }) : "",
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {href} {target} {rel} class="{base} {interactive} {className}">
|
||||
<a {href} {target} {rel} class="{surface} {linkSurface} {className}">
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
<div class="{base} {className}">
|
||||
<div class="{surface} {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
<div><span class="opacity-20">|</span></div>
|
||||
<div class="w-1 h-4"></div>
|
||||
{#each socialLinks as social}
|
||||
<a
|
||||
href={social.href}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import PostMeta from "./PostMeta.svelte";
|
||||
import EventBadges from "./EventBadges.svelte";
|
||||
import RustyImage from "./RustyImage.svelte";
|
||||
import Card from "./Card.svelte";
|
||||
|
||||
type PostWithImage = PostEntry & {
|
||||
_resolvedImageUrl?: string;
|
||||
@@ -45,9 +46,10 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={href}
|
||||
class="transition-all flex flex-col text-slate-950 no-underline overflow-hidden border border-gray-200 bg-white relative {isPastEvent ? 'opacity-70 grayscale' : ''}"
|
||||
<Card
|
||||
{href}
|
||||
layout="post"
|
||||
class={isPastEvent ? "opacity-70 grayscale" : ""}
|
||||
>
|
||||
<div class="relative aspect-3/2 w-full overflow-hidden bg-gray-100">
|
||||
{#if rawImg}
|
||||
@@ -110,4 +112,4 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Card>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<div class="w-full">
|
||||
{#if hasImage}
|
||||
<div
|
||||
class="w-full overflow-hidden relative flex justify-center items-center shadow-lg border-b border-white bg-zinc-200"
|
||||
class="w-full overflow-hidden relative flex justify-center items-center shadow-lg bg-zinc-200"
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{#if hasResponsive && responsive}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
? "bg-amber-100 text-amber-900"
|
||||
: variant === "info"
|
||||
? "bg-sky-100 text-sky-900"
|
||||
: "bg-wald-100 text-wald-900",
|
||||
: "bg-wald-500 text-wald-50",
|
||||
);
|
||||
const iconName = $derived(
|
||||
variant === "urgent" ? "mdi:alert-circle" : "mdi:calendar-clock",
|
||||
@@ -99,30 +99,35 @@
|
||||
data-block-slug={block._slug}
|
||||
>
|
||||
{#snippet inner()}
|
||||
<div class="container-custom flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-1 px-4 py-1.5 text-xs sm:text-sm">
|
||||
<div class="container-custom flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-1 px-4 py-2 text-xs sm:text-sm">
|
||||
{#if dateStr}
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md bg-white/70 px-2 py-0.5 text-sm sm:text-base font-bold whitespace-nowrap shrink-0">
|
||||
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" />
|
||||
{dateStr}
|
||||
<span class="inline-flex items-center rounded-md bg-white/50 text-wald-900 px-2 py-0.5 text-sm sm:text-base font-bold whitespace-nowrap shrink-0">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" />
|
||||
{dateStr}
|
||||
</span>
|
||||
{#if countdownStr}
|
||||
<span class="ml-1 rounded-full bg-black/80 text-white px-1.5 py-0 text-[0.6rem] font-bold uppercase tracking-wide">
|
||||
<span
|
||||
class="ml-0 inline-flex min-w-0 max-w-0 scale-[0.96] items-center overflow-hidden whitespace-nowrap rounded-md border border-transparent py-0.5 text-[0.6rem] font-semibold uppercase tracking-wide opacity-0 transition-[max-width,margin,opacity,transform,padding,border-color] duration-200 ease-out group-hover:ml-2 group-hover:max-w-48 group-hover:border-wald-900 group-hover:px-2 group-hover:opacity-100 group-hover:scale-100 group-focus-within:ml-2 group-focus-within:max-w-48 group-focus-within:border-wald-900 group-focus-within:px-2 group-focus-within:opacity-100 group-focus-within:scale-100"
|
||||
>
|
||||
{countdownStr}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" />
|
||||
{#if countdownStr}
|
||||
<span class="rounded-full bg-white/60 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide whitespace-nowrap shrink-0">
|
||||
{countdownStr}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="inline-flex items-center gap-0 shrink-0">
|
||||
<Icon icon={iconName} class="size-4 shrink-0" aria-hidden="true" />
|
||||
{#if countdownStr}
|
||||
<span
|
||||
class="ml-0 inline-flex min-w-0 max-w-0 shrink-0 scale-[0.96] items-center overflow-hidden whitespace-nowrap rounded-full px-0 py-0.5 text-xs font-semibold uppercase tracking-wide opacity-0 transition-[max-width,margin,opacity,transform,padding] duration-200 ease-out group-hover:ml-2 group-hover:max-w-48 group-hover:px-2 group-hover:opacity-100 group-hover:scale-100 group-focus-within:ml-2 group-focus-within:max-w-48 group-focus-within:px-2 group-focus-within:opacity-100 group-focus-within:scale-100"
|
||||
>
|
||||
{countdownStr}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
{#if text}
|
||||
<span class="min-w-0 wrap-break-word line-clamp-2 sm:line-clamp-1">{text}</span>
|
||||
{/if}
|
||||
{#if href}
|
||||
<Icon icon="mdi:arrow-right" class="size-4 shrink-0 ml-auto" aria-hidden="true" />
|
||||
<span class="min-w-0 wrap-break-word line-clamp-2 sm:line-clamp-1 font-semibold text-shadow-sm">{text}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -131,12 +136,12 @@
|
||||
href={href}
|
||||
target={newTab ? "_blank" : undefined}
|
||||
rel={newTab ? "noopener noreferrer" : undefined}
|
||||
class="block no-underline text-inherit hover:no-underline transition-all hover:brightness-95 {variantClasses}"
|
||||
class="group block no-underline text-inherit hover:no-underline transition-all hover:brightness-95 {variantClasses}"
|
||||
>
|
||||
{@render inner()}
|
||||
</a>
|
||||
{:else}
|
||||
<div class="{variantClasses}">
|
||||
<div class="group {variantClasses}">
|
||||
{@render inner()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -56,10 +56,6 @@
|
||||
)
|
||||
);
|
||||
|
||||
const addOrganisationCardClass = $derived(
|
||||
`border-dashed border-wald-300 bg-wald-50! p-3! gap-1.5!${addOrganisationCtaLink ? " cursor-pointer" : ""}`,
|
||||
);
|
||||
|
||||
let scroller: HTMLDivElement | undefined = $state();
|
||||
function scrollByPage(dir: 1 | -1) {
|
||||
if (!scroller) return;
|
||||
@@ -68,8 +64,8 @@
|
||||
</script>
|
||||
|
||||
{#snippet defaultCard(o: OrganisationEntry, linkUrl: string | undefined, logoField: ReturnType<typeof organisationLogoField>, orgIndex: number)}
|
||||
<Card href={linkUrl} class="p-3! gap-1.5! h-full">
|
||||
<div class="h-11 w-11 rounded-md border border-stein-100 bg-stein-50 flex items-center justify-center overflow-hidden shrink-0">
|
||||
<Card href={linkUrl} class="h-full">
|
||||
<div class="flex h-11 w-11 shrink-0 items-center justify-center overflow-hidden bg-gray-100">
|
||||
{#if logoField}
|
||||
<RustyImage
|
||||
src={logoField.url}
|
||||
@@ -84,7 +80,7 @@
|
||||
/>
|
||||
{:else}
|
||||
{@const fishMaskId = `org-logo-fallback-mask-${orgIndex}`}
|
||||
<svg class="h-8 w-8 text-stein-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
|
||||
<svg class="h-8 w-8 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
|
||||
<defs>
|
||||
<mask id={fishMaskId}>
|
||||
<g fill="none">
|
||||
@@ -103,19 +99,20 @@
|
||||
<Badge color={o.badge.color}>{o.badge.label}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
<h5 class="min-w-0 text-sm font-semibold leading-snug text-stein-900 truncate">{o.name ?? ""}</h5>
|
||||
<h5 class="mb-1 min-w-0 truncate text-sm font-medium leading-tight text-slate-950">{o.name ?? ""}</h5>
|
||||
{#if o.description}
|
||||
<p class="text-xs text-stein-600 line-clamp-2">{o.description}</p>
|
||||
<p class="line-clamp-2 text-xs font-light text-slate-600">{o.description}</p>
|
||||
{/if}
|
||||
</Card>
|
||||
{/snippet}
|
||||
|
||||
{#snippet ctaCard()}
|
||||
<Card
|
||||
variant="callout"
|
||||
href={addOrganisationCtaLink?.url}
|
||||
target={addOrganisationCtaLink?.newTab ? "_blank" : undefined}
|
||||
rel={addOrganisationCtaLink?.newTab ? "noopener noreferrer" : undefined}
|
||||
class="{addOrganisationCardClass} h-full"
|
||||
class="h-full"
|
||||
>
|
||||
{#if block.addOrganisationTitle}
|
||||
<h5 class="text-sm font-semibold text-wald-800 leading-snug">{block.addOrganisationTitle}</h5>
|
||||
@@ -138,7 +135,7 @@
|
||||
<div class="relative">
|
||||
<div
|
||||
bind:this={scroller}
|
||||
class="flex gap-3 overflow-x-auto scroll-smooth snap-x snap-mandatory pb-2 -mx-2 px-2 [scrollbar-width:thin]"
|
||||
class="flex gap-4 overflow-x-auto scroll-smooth snap-x snap-mandatory pb-2 [scrollbar-width:thin]"
|
||||
>
|
||||
{#each orgs as org, orgIndex}
|
||||
{#if typeof org === "object" && org !== null}
|
||||
@@ -151,7 +148,9 @@
|
||||
? o.link.url.trim()
|
||||
: undefined}
|
||||
{@const logoField = organisationLogoField(o)}
|
||||
<div class="shrink-0 snap-start basis-1/2 lg:basis-1/3">
|
||||
<div
|
||||
class="shrink-0 snap-start w-[calc((100%-1rem)/2)] lg:w-[calc((100%-2rem)/3)]"
|
||||
>
|
||||
{@render defaultCard(o, linkUrl, logoField, orgIndex)}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -177,27 +176,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if showAddOrganisationCard}
|
||||
<div class="mt-4 rounded-md border border-dashed border-wald-300 bg-wald-50 p-3 text-sm text-wald-800">
|
||||
{#if block.addOrganisationTitle}
|
||||
<div class="font-semibold">{block.addOrganisationTitle}</div>
|
||||
{/if}
|
||||
{#if ctaHtml}
|
||||
<div class="markdown text-xs text-wald-700 [&>p]:mb-0">{@html ctaHtml}</div>
|
||||
{/if}
|
||||
{#if addOrganisationCtaLink}
|
||||
<a
|
||||
href={addOrganisationCtaLink.url}
|
||||
target={addOrganisationCtaLink.newTab ? "_blank" : undefined}
|
||||
rel={addOrganisationCtaLink.newTab ? "noopener noreferrer" : undefined}
|
||||
class="mt-1 inline-block font-medium underline-offset-2 hover:underline"
|
||||
>
|
||||
{addOrganisationCtaLink.linkName}
|
||||
</a>
|
||||
{/if}
|
||||
<div class="mt-2 min-w-0">
|
||||
{@render ctaCard()}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class={compact ? "grid gap-2 sm:grid-cols-2 lg:grid-cols-3" : "grid gap-4 sm:grid-cols-2 lg:grid-cols-3"}>
|
||||
<div
|
||||
class={compact
|
||||
? "grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3"
|
||||
: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"}
|
||||
>
|
||||
{#each orgs as org, orgIndex}
|
||||
{#if typeof org === "object" && org !== null}
|
||||
{@const o = org as OrganisationEntry}
|
||||
@@ -210,8 +198,8 @@
|
||||
: undefined}
|
||||
{@const logoField = organisationLogoField(o)}
|
||||
{#if compact}
|
||||
<Card href={linkUrl} class="flex! flex-row! items-start! gap-3! p-3! gap-y-0!">
|
||||
<div class="h-10 w-10 rounded-md border border-stein-100 bg-stein-50 flex items-center justify-center overflow-hidden shrink-0">
|
||||
<Card href={linkUrl} layout="inline" class="h-full">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden bg-gray-100">
|
||||
{#if logoField}
|
||||
<RustyImage
|
||||
src={logoField.url}
|
||||
@@ -226,7 +214,7 @@
|
||||
/>
|
||||
{:else}
|
||||
{@const fishMaskId = `org-logo-fallback-mask-c-${orgIndex}`}
|
||||
<svg class="h-7 w-7 text-stein-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
|
||||
<svg class="h-7 w-7 text-gray-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="currentColor" aria-hidden="true">
|
||||
<defs>
|
||||
<mask id={fishMaskId}>
|
||||
<g fill="none">
|
||||
@@ -240,10 +228,10 @@
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 flex flex-col gap-0.5">
|
||||
<h5 class="text-sm font-semibold text-stein-900 leading-snug">{o.name ?? ""}</h5>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<h5 class="text-sm font-medium leading-tight text-slate-950">{o.name ?? ""}</h5>
|
||||
{#if o.description}
|
||||
<p class="text-xs text-stein-600 line-clamp-2">{o.description}</p>
|
||||
<p class="line-clamp-2 text-xs font-light text-slate-600">{o.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user