refactor(badge): eine Badge-Komponente überall, alle 7 CMS-Farben
Deploy / verify (push) Successful in 1m36s
Deploy / deploy (push) Successful in 1m42s

components/Badge.svelte = einzige Badge-Quelle. colorClasses deckt jetzt alle
CMS-badge.color-Werte ab (green/blue/amber/orange/purple/teal/slate) — vorher
fielen orange/purple/teal/slate auf grau (Regionen-Badges sahen gleich aus).

Ad-hoc Badge-Spans (ContactCard, AdressbuchBlock) durch <Badge> ersetzt.
Verwaiste ui/Badge.svelte (0 Nutzungen) + Atoms/Badge-Story entfernt.
Molecules/Badge-Story zeigt jetzt alle Farben.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-07-12 09:26:48 +02:00
parent b60da59c92
commit 7c7538a22c
7 changed files with 36 additions and 94 deletions
+15 -3
View File
@@ -6,9 +6,21 @@
title: 'Molecules/Badge',
component: Badge,
tags: ['autodocs'],
argTypes: {
color: {
control: 'select',
options: ['green', 'blue', 'amber', 'orange', 'purple', 'teal', 'slate', undefined],
},
},
});
</script>
<Story name="Standard">Neu</Story>
<Story name="Wald" args={{ color: 'wald' }}>Aktiv</Story>
<Story name="Fire" args={{ color: 'fire' }}>Dringend</Story>
<!-- Die eine Badge. `color` deckt alle CMS-badge.color-Werte ab. -->
<Story name="Default">Neu</Story>
<Story name="Green" args={{ color: 'green' }}>Gründer</Story>
<Story name="Blue" args={{ color: 'blue' }}>Mitglied</Story>
<Story name="Amber" args={{ color: 'amber' }}>Anfrage läuft</Story>
<Story name="Orange" args={{ color: 'orange' }}>Südwestthüringen</Story>
<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>
+15 -5
View File
@@ -7,16 +7,26 @@
class: className = "",
}: { children?: Snippet; color?: string; 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 {
switch (c) {
case "green": return "bg-wald-50 text-wald-700 border border-wald-200";
case "blue": return "bg-himmel-50 text-himmel-700 border border-himmel-200";
case "amber": return "bg-erde-50 text-erde-700 border border-erde-200";
default: return "bg-stein-100 text-stein-600 border border-stein-200";
case "green": return "bg-wald-50 text-wald-700 border-wald-200";
case "blue": return "bg-himmel-50 text-himmel-700 border-himmel-200";
case "amber": return "bg-amber-50 text-amber-700 border-amber-200";
case "orange": return "bg-orange-50 text-orange-700 border-orange-200";
case "purple": return "bg-purple-50 text-purple-700 border-purple-200";
case "teal": return "bg-teal-50 text-teal-700 border-teal-200";
case "slate": return "bg-slate-100 text-slate-700 border-slate-300";
default: return "bg-stein-100 text-stein-600 border-stein-200";
}
}
</script>
<span class="shrink-0 rounded-full px-1.5 py-0 text-[0.65rem] font-medium {colorClasses(color)} {className}">
<span
class="shrink-0 whitespace-nowrap rounded-full border px-1.5 py-0 text-[0.65rem] font-medium {colorClasses(color)} {className}"
>
{@render children?.()}
</span>
+3 -6
View File
@@ -3,6 +3,7 @@
import "$lib/iconify-offline";
import type { ContactEntry } from "$lib/block-types";
import { downloadVCard } from "$lib/vcard";
import Badge from "$lib/components/Badge.svelte";
let { contact }: { contact: ContactEntry } = $props();
@@ -23,14 +24,10 @@
{#if contact.role || orgNames.length > 0}
<div class="flex flex-wrap gap-1">
{#if contact.role}
<span class="shrink-0 rounded-full border border-stein-200 bg-stein-100 px-1.5 py-0 text-[0.65rem] font-medium text-stein-600">
{contact.role}
</span>
<Badge>{contact.role}</Badge>
{/if}
{#each orgNames as org}
<span class="shrink-0 rounded-full border border-wald-200 bg-wald-50 px-1.5 py-0 text-[0.65rem] font-medium text-wald-700">
{org}
</span>
<Badge color="green">{org}</Badge>
{/each}
</div>
{/if}
@@ -6,6 +6,7 @@
import { downloadVCard, downloadVCardAll, buildVCardString } from "$lib/vcard";
import QrButton from "$lib/components/QrButton.svelte";
import Button from "$lib/ui/Button.svelte";
import Badge from "$lib/components/Badge.svelte";
let { block }: { block: AdressbuchBlockData } = $props();
@@ -241,14 +242,10 @@
{#if contact.role || (contact.organisations ?? []).some((o) => typeof o === "object" && o.name)}
<div class="mt-0.5 flex flex-nowrap gap-1 overflow-hidden">
{#if contact.role}
<span class="shrink-0 whitespace-nowrap rounded-full border border-stein-200 bg-stein-100 px-1.5 py-0 text-[0.65rem] font-medium text-stein-600">
{contact.role}
</span>
<Badge>{contact.role}</Badge>
{/if}
{#each (contact.organisations ?? []).filter((o): o is { _slug?: string; name?: string } => typeof o === "object" && !!o.name) as org}
<span class="shrink-0 whitespace-nowrap rounded-full border border-wald-200 bg-wald-50 px-1.5 py-0 text-[0.65rem] font-medium text-wald-700">
{org.name}
</span>
<Badge color="green">{org.name}</Badge>
{/each}
</div>
{/if}
-16
View File
@@ -1,16 +0,0 @@
import type { Meta, StoryObj } from '@storybook/sveltekit';
import Badge from './Badge.svelte';
const meta = {
title: 'Atoms/Badge',
component: Badge,
tags: ['autodocs'],
args: { label: 'Neu' },
} satisfies Meta<typeof Badge>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const AlsLink: Story = { args: { label: 'Mehr', href: '#' } };
export const Aktiv: Story = { args: { label: 'Aktiv', active: true } };
-57
View File
@@ -1,57 +0,0 @@
<script lang="ts">
type Variant = 'default' | 'primary' | 'accent' | 'inactive' | 'date';
let {
label = '',
variant = 'default',
href = null as string | null,
active = false,
onclick = null as (() => void) | null,
} = $props();
const baseClass =
'inline-flex shrink-0 whitespace-nowrap rounded-full py-1 px-2.5 text-[0.6875rem] font-medium transition-colors ring-1 no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1';
const variantClasses = $derived(
{
default: 'bg-stein-0 text-stein-800 ring-stein-200 hover:bg-stein-50',
primary: 'bg-wald-500 text-white ring-wald-600/30 hover:bg-wald-600',
accent: 'bg-himmel-400 text-white ring-himmel-500/30 hover:bg-himmel-500',
inactive: 'bg-stein-100 text-stein-500 ring-stein-200',
date: 'bg-wald-600 text-white ring-wald-700/40',
}[variant]
);
const shadowClass = $derived(
href || onclick
? variant === 'date'
? 'shadow-[0_0_6px_rgba(0,0,0,0.25)]'
: 'shadow-[0_2px_6px_rgba(0,0,0,0.12)]'
: ''
);
const classes = $derived(
`${baseClass} ${variantClasses} ${shadowClass} ${active ? 'pointer-events-none ring-2' : ''}`
);
</script>
{#if href}
<a
href={href}
class={classes}
aria-current={active ? 'true' : undefined}
>
{label}
</a>
{:else if onclick}
<button
type="button"
class={classes}
aria-current={active ? 'true' : undefined}
onclick={onclick}
>
{label}
</button>
{:else}
<span class={classes}>{label}</span>
{/if}
-1
View File
@@ -12,5 +12,4 @@ export { default as Toggle } from './Toggle.svelte';
export { default as Checkbox } from './Checkbox.svelte';
export { default as Radio } from './Radio.svelte';
export { default as Slider } from './Slider.svelte';
export { default as Badge } from './Badge.svelte';
export { default as Tooltip } from './Tooltip.svelte';