A self-hosted orchestration system crafted for power users. composia.xyz
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210<script lang="ts"> import { Search } from "@lucide/svelte"; import type { Snippet } from "svelte";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { Badge } from "$lib/components/ui/badge"; import { Button } from "$lib/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, } from "$lib/components/ui/card"; import { Input } from "$lib/components/ui/input"; import { Skeleton } from "$lib/components/ui/skeleton"; import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNextButton, PaginationPrevButton, } from "$lib/components/ui/pagination"; import { getMessages } from "$lib/i18n";
const messages = getMessages();
interface Props { title: string; subtitle: string; backHref: string; backLabel: string; totalCount: number; pageSize: number; itemCount: number; totalPages: number; ready: boolean; loading: boolean; error: string | null; searchId: string; searchPlaceholder: string; loadingText: string; emptyText: string; noResultsText: string; countSummary?: string; searchQuery: string; currentPage: number; onSearchInput: () => void; onClearSearch: () => void; onRefresh: () => void | Promise<void>; toolbarActions?: Snippet; children?: Snippet; }
let { title, subtitle, backHref, backLabel, totalCount, pageSize, itemCount, totalPages, ready, loading, error, searchId, searchPlaceholder, loadingText, emptyText, noResultsText, countSummary, searchQuery = $bindable(), currentPage = $bindable(), onSearchInput, onClearSearch, onRefresh, toolbarActions, children, }: Props = $props();</script>
<Card> <CardHeader> <div class="page-header"> <div class="page-heading"> <CardTitle class="page-title" level="1">{title}</CardTitle> <p class="page-description"> {subtitle} {#if !loading} <Badge variant="outline" class="ml-2">{totalCount}</Badge> {/if} </p> </div> <a href={backHref} class="text-sm text-muted-foreground transition-colors hover:text-foreground" > {backLabel} </a> </div>
<div class="flex items-center gap-3"> <div class="relative max-w-sm flex-1"> <label class="sr-only" for={searchId}>{searchPlaceholder}</label> <Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" /> <Input id={searchId} type="text" placeholder={searchPlaceholder} aria-label={searchPlaceholder} class="pl-9" bind:value={searchQuery} oninput={onSearchInput} /> </div> {#if searchQuery} <Button variant="ghost" size="sm" onclick={onClearSearch}> {$messages.common.cancel} </Button> {/if} {@render toolbarActions?.()} <Button variant="outline" size="sm" onclick={() => void onRefresh()} disabled={loading || !ready} > {#if loading}{$messages.common.loading}...{:else}{$messages.common .refresh}{/if} </Button> </div> </CardHeader> <CardContent aria-busy={loading}> {#if error} <Alert variant="destructive"> <AlertDescription>{error}</AlertDescription> </Alert> {:else if loading} <div class="min-h-80 space-y-3 py-2" role="status" aria-live="polite"> <span class="sr-only">{loadingText}</span> {#each Array(7) as _} <div class="grid min-h-12 grid-cols-4 items-center gap-4 border-b py-3" > <Skeleton class="h-4 w-4/5" /> <Skeleton class="h-4 w-3/5" /> <Skeleton class="h-4 w-2/3" /> <Skeleton class="h-8 w-20 justify-self-end" /> </div> {/each} </div> {:else if itemCount > 0} {@render children?.()}
{#if countSummary} <div class="mt-3 text-center text-xs text-muted-foreground"> {countSummary} </div> {/if}
{#if totalPages > 1} <div class="mt-6"> <Pagination count={totalCount} perPage={pageSize} bind:page={currentPage} > {#snippet children({ pages, currentPage })} <PaginationContent> <PaginationItem> <PaginationPrevButton /> </PaginationItem>
{#each pages as page (page.key)} {#if page.type === "ellipsis"} <PaginationItem> <PaginationEllipsis /> </PaginationItem> {:else} <PaginationItem> <PaginationLink {page} isActive={currentPage === page.value} /> </PaginationItem> {/if} {/each}
<PaginationItem> <PaginationNextButton /> </PaginationItem> </PaginationContent> {/snippet} </Pagination> </div> {/if} {:else if searchQuery} <div class="empty-state"> {noResultsText} </div> {:else} <div class="empty-state">{emptyText}</div> {/if} </CardContent></Card>