File size: 3,071 Bytes
336b102
 
 
 
 
 
236e9a2
 
336b102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236e9a2
 
 
 
336b102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236e9a2
336b102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236e9a2
336b102
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<script lang="ts" module>
	import type { Snippet } from 'svelte';

	export type TableColumn<Key extends string = string> = {
		key: Key;
		label: string | (() => string);
		headClass?: string | (() => string | undefined);
		cellClass?: string | (() => string | undefined);
		ariaSort?: () => 'ascending' | 'descending' | 'none';
	};
</script>

<script lang="ts" generics="Row, Column extends TableColumn">
	import * as UITable from '$lib/components/ui/table';
	import { cn } from '$lib/utils.js';

	type Props = {
		title?: string;
		data: Row[];
		columns: Column[];
		getRowKey: (row: Row, index: number) => string | number;
		rowLabel?: (row: Row, index: number) => string;
		rowClass?: string | ((row: Row, index: number) => string);
		containerClass?: string;
		tableClass?: string;
		headerClass?: string;
		onrowclick?: (row: Row, index: number) => void;
		header?: Snippet<[Column]>;
		cell: Snippet<[Row, Column, number]>;
	};

	let {
		title,
		data,
		columns,
		getRowKey,
		rowLabel,
		rowClass,
		containerClass = 'mt-4 overflow-hidden rounded-2xl border bg-card',
		tableClass = 'min-w-[980px]',
		headerClass = 'bg-muted/50 hover:bg-muted/50 [&_th]:text-muted-foreground',
		onrowclick,
		header,
		cell
	}: Props = $props();

	function labelText(label: string | (() => string)) {
		return typeof label === 'function' ? label() : label;
	}

	function resolvedRowClass(row: Row, index: number) {
		return typeof rowClass === 'function' ? rowClass(row, index) : rowClass;
	}

	function resolvedColumnClass(className: string | (() => string | undefined) | undefined) {
		return typeof className === 'function' ? className() : className;
	}

	function handleRowKeydown(event: KeyboardEvent, row: Row, index: number) {
		if (!onrowclick || (event.key !== 'Enter' && event.key !== ' ')) return;

		event.preventDefault();
		onrowclick(row, index);
	}
</script>

<section class={containerClass}>
	{#if title}
		<h2 class="sr-only">{title}</h2>
	{/if}

	<UITable.Root class={tableClass}>
		<UITable.Header>
			<UITable.Row class={headerClass}>
				{#each columns as column (column.key)}
					<UITable.Head
						class={cn('text-muted-foreground', resolvedColumnClass(column.headClass))}
						aria-sort={column.ariaSort?.() ?? 'none'}
					>
						{#if header}
							{@render header(column)}
						{:else}
							{labelText(column.label)}
						{/if}
					</UITable.Head>
				{/each}
			</UITable.Row>
		</UITable.Header>
		<UITable.Body>
			{#each data as row, index (getRowKey(row, index))}
				<UITable.Row
					class={resolvedRowClass(row, index)}
					role={onrowclick ? 'button' : undefined}
					tabindex={onrowclick ? 0 : undefined}
					aria-label={rowLabel?.(row, index)}
					onclick={() => onrowclick?.(row, index)}
					onkeydown={(event) => handleRowKeydown(event, row, index)}
				>
					{#each columns as column (column.key)}
						<UITable.Cell class={resolvedColumnClass(column.cellClass)}>
							{@render cell(row, column, index)}
						</UITable.Cell>
					{/each}
				</UITable.Row>
			{/each}
		</UITable.Body>
	</UITable.Root>
</section>