initial production version

This commit is contained in:
2025-11-25 16:08:50 +01:00
parent 44ce6e38dd
commit 0144e8df1a
108 changed files with 5502 additions and 1780 deletions

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
import type { Snippet } from 'svelte';
import { getCardStyle } from '$lib/utils/colors';
let {
title,
description,
itemCount,
color = null,
children
}: {
title: string;
description?: string | null;
itemCount: number;
color?: string | null;
children?: Snippet;
} = $props();
const cardStyle = $derived(getCardStyle(color));
</script>
<Card style={cardStyle} class="h-full flex flex-col">
<CardHeader class="flex-shrink-0">
<div class="flex items-center justify-between gap-2">
<CardTitle class="text-lg flex items-center gap-2 flex-1 min-w-0">
<span class="truncate">{title}</span>
</CardTitle>
<span class="text-sm text-muted-foreground flex-shrink-0">
{itemCount} item{itemCount === 1 ? '' : 's'}
</span>
</div>
{#if description}
<CardDescription class="line-clamp-3 whitespace-pre-line">{description}</CardDescription>
{/if}
</CardHeader>
<CardContent class="space-y-2 flex-1 flex flex-col justify-end">
{#if children}
<div>
{@render children()}
</div>
{/if}
</CardContent>
</Card>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import EmptyState from '$lib/components/layout/EmptyState.svelte';
import type { Snippet } from 'svelte';
import { flip } from 'svelte/animate';
let {
title,
description,
items,
emptyMessage,
emptyDescription,
emptyActionLabel,
emptyActionHref,
headerAction,
children
}: {
title: string;
description: string;
items: any[];
emptyMessage: string;
emptyDescription?: string;
emptyActionLabel?: string;
emptyActionHref?: string;
headerAction?: Snippet;
children: Snippet<[any]>;
} = $props();
let scrollContainer: HTMLElement | null = null;
function handleWheel(event: WheelEvent) {
if (!scrollContainer) return;
// Check if we have horizontal overflow
const hasHorizontalScroll = scrollContainer.scrollWidth > scrollContainer.clientWidth;
if (hasHorizontalScroll && event.deltaY !== 0) {
event.preventDefault();
scrollContainer.scrollLeft += event.deltaY;
}
}
</script>
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<div>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</div>
{#if headerAction}
{@render headerAction()}
{/if}
</div>
</CardHeader>
<CardContent>
{#if items && items.length > 0}
<div
bind:this={scrollContainer}
onwheel={handleWheel}
class="flex overflow-x-auto gap-4 pb-4 -mx-6 px-6"
>
{#each items as item (item.id)}
<div class="flex-shrink-0 w-80" animate:flip={{ duration: 300 }}>
{@render children(item)}
</div>
{/each}
</div>
{:else}
<EmptyState
message={emptyMessage}
description={emptyDescription}
actionLabel={emptyActionLabel}
actionHref={emptyActionHref}
/>
{/if}
</CardContent>
</Card>