mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 07:19:40 +02:00
PageShell header icon/title had opacity: 0.5 — removed for full visibility. Moodlit, Zitare, Skilltree and BaseListView used text-white/* classes that were invisible in light mode — migrated to hsl(var(--color-foreground/muted-foreground)) tokens. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
101 lines
2.8 KiB
Svelte
101 lines
2.8 KiB
Svelte
<script lang="ts" generics="T">
|
|
/**
|
|
* BaseListView — shared scaffolding for module ListView components.
|
|
*
|
|
* Encodes the workbench convention every Mana module's ListView shares:
|
|
* wrapper padding → optional stats header → scrollable item region → empty state.
|
|
*
|
|
* Per-item rendering and data fetching stay with the consumer:
|
|
* - Pass `items` (already filtered & decrypted via queries.ts).
|
|
* - Provide an `item` snippet that renders one row.
|
|
* - Provide an optional `header` snippet for stat counts or filters.
|
|
*
|
|
* @example
|
|
* ```svelte
|
|
* <BaseListView items={sorted} getKey={(q) => q.id} emptyTitle="Keine Fragen">
|
|
* {#snippet header()}
|
|
* <span>{questions.length} Fragen</span>
|
|
* {/snippet}
|
|
* {#snippet item(question)}
|
|
* <button onclick={() => navigate('detail', { id: question.id })}>
|
|
* {question.title}
|
|
* </button>
|
|
* {/snippet}
|
|
* </BaseListView>
|
|
* ```
|
|
*/
|
|
import type { Snippet } from 'svelte';
|
|
import { EmptyState } from '../molecules';
|
|
|
|
interface Props<TItem> {
|
|
/** Items to render. Should already be filtered (deletedAt) and decrypted. */
|
|
items: TItem[];
|
|
/** Stable key extractor for the {#each} block. */
|
|
getKey: (item: TItem) => string | number;
|
|
/** Snippet that renders a single item row. */
|
|
item: Snippet<[TItem, number]>;
|
|
/** Optional header snippet (e.g. stat counts, filters). */
|
|
header?: Snippet;
|
|
/** Optional snippet rendered above the items but inside the scroll area. */
|
|
listHeader?: Snippet;
|
|
/** Optional snippet rendered at the very top, outside the scroll area (toolbar, voice bar, ...). */
|
|
toolbar?: Snippet;
|
|
/** Empty-state title. */
|
|
emptyTitle?: string;
|
|
/** Empty-state message. */
|
|
emptyMessage?: string;
|
|
/** Custom empty-state icon snippet. */
|
|
emptyIcon?: Snippet;
|
|
/** Override the entire empty area. */
|
|
empty?: Snippet;
|
|
/** Optional outer class override. */
|
|
class?: string;
|
|
/** Optional class for the inner scroll/list area. Use this to switch to grid, etc. */
|
|
listClass?: string;
|
|
}
|
|
|
|
let {
|
|
items,
|
|
getKey,
|
|
item,
|
|
header,
|
|
listHeader,
|
|
toolbar,
|
|
emptyTitle = 'Nichts hier',
|
|
emptyMessage,
|
|
emptyIcon,
|
|
empty,
|
|
class: className = '',
|
|
listClass = '',
|
|
}: Props<T> = $props();
|
|
</script>
|
|
|
|
<div class="flex h-full flex-col gap-3 p-3 sm:p-4 {className}">
|
|
{#if toolbar}
|
|
{@render toolbar()}
|
|
{/if}
|
|
|
|
{#if header}
|
|
<div class="flex gap-3 text-xs text-[hsl(var(--color-muted-foreground))]">
|
|
{@render header()}
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="flex-1 overflow-auto {listClass}">
|
|
{#if listHeader}
|
|
{@render listHeader()}
|
|
{/if}
|
|
|
|
{#each items as entry, i (getKey(entry))}
|
|
{@render item(entry, i)}
|
|
{/each}
|
|
|
|
{#if items.length === 0}
|
|
{#if empty}
|
|
{@render empty()}
|
|
{:else}
|
|
<EmptyState variant="compact" title={emptyTitle} message={emptyMessage} icon={emptyIcon} />
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|