mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
refactor(mana/web): consolidate ListView scaffolding into BaseListView
Every workbench-style module ListView reimplemented the same liveQuery + filter + scroll-area + empty-state shell. Extract a shared <BaseListView> in @mana/shared-ui (with toolbar/header/ listHeader/item/empty snippets) and migrate the 17 modules whose list templates fit the workbench tailwind track. While here: - migrate DeckCard onto the existing (previously unused) shared Card atom from shared-ui/atoms. - fix a latent type bug in times/ListView: it was reading .date / .startTime / .isRunning off LocalTimeEntry, which doesn't define them. Now uses the proper joined TimeEntry via toTimeEntry() like the rest of the times module. Modules with their own scoped-CSS layout track (calendar, finance, contacts, notes, places, todo, photos, habits, automations, dreams, cycles) and outliers (calc, events, playground, zitare) are left alone — migrating them would be a visual rewrite, not a structural shell swap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d941ff2231
commit
c3cb9dd533
21 changed files with 878 additions and 1044 deletions
|
|
@ -67,7 +67,7 @@ export { ModalFooter, DataCard, PageHeader, KeyboardShortcutsPanel } from './mol
|
|||
export { ConfirmationPopover } from './molecules';
|
||||
|
||||
// Organisms
|
||||
export { Modal, ConfirmationModal, FormModal, AppSlider } from './organisms';
|
||||
export { Modal, ConfirmationModal, FormModal, AppSlider, BaseListView } from './organisms';
|
||||
export type { AppItem } from './organisms';
|
||||
|
||||
// Network Graph
|
||||
|
|
|
|||
101
packages/shared-ui/src/organisms/BaseListView.svelte
Normal file
101
packages/shared-ui/src/organisms/BaseListView.svelte
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<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-white/40">
|
||||
{@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>
|
||||
|
|
@ -2,6 +2,7 @@ export { default as Modal } from './Modal.svelte';
|
|||
export { default as ConfirmationModal } from './ConfirmationModal.svelte';
|
||||
export { default as FormModal } from './FormModal.svelte';
|
||||
export { default as AppSlider } from './AppSlider.svelte';
|
||||
export { default as BaseListView } from './BaseListView.svelte';
|
||||
export type { AppItem } from './AppSlider.types';
|
||||
|
||||
// Network Graph
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue