feat: redesign match play and add broadcast tooling
This commit is contained in:
@@ -16,13 +16,16 @@
|
||||
meta,
|
||||
columns,
|
||||
row,
|
||||
searchPlaceholder = 'Search...'
|
||||
searchPlaceholder = 'Search...',
|
||||
layout = 'table'
|
||||
}: {
|
||||
data: T[];
|
||||
meta: TableMeta;
|
||||
columns: Column[];
|
||||
row: Snippet<[T, number]>;
|
||||
searchPlaceholder?: string;
|
||||
/** `list` renders rows as cards (the row snippet gives each card), with sort buttons instead of a header. */
|
||||
layout?: 'table' | 'list';
|
||||
} = $props();
|
||||
|
||||
let searchInput = $derived.by(() => meta.search);
|
||||
@@ -115,72 +118,104 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- table -->
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-border bg-surface-800">
|
||||
{#each columns as col}
|
||||
<th
|
||||
class="{col.class ??
|
||||
'px-4 text-left'} py-3 text-[11px] font-semibold tracking-wider text-text-secondary uppercase"
|
||||
>
|
||||
{#if col.sortable}
|
||||
<button
|
||||
class="inline-flex items-center gap-1 transition-colors hover:text-text-primary"
|
||||
onclick={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.label}
|
||||
{#if meta.sortBy === col.key}
|
||||
<svg
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
{#if meta.sortDir === 'asc'}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
{/if}
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="h-3 w-3 opacity-30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
{col.label}
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border/50">
|
||||
{#each data as item, i}
|
||||
{@render row(item, i)}
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan={columns.length} class="px-4 py-8 text-center text-sm text-text-secondary">
|
||||
{meta.search ? `No results for "${meta.search}"` : 'No data'}
|
||||
</td>
|
||||
</tr>
|
||||
{#if layout === 'list'}
|
||||
{#if columns.some((col) => col.sortable)}
|
||||
<div class="mb-3 flex flex-wrap items-center gap-1.5 text-xs text-text-secondary">
|
||||
<span>Sort by</span>
|
||||
{#each columns.filter((col) => col.sortable) as col (col.key)}
|
||||
<button
|
||||
class="rounded-md border px-2 py-1 transition-colors {meta.sortBy === col.key
|
||||
? 'border-white/20 bg-white/5 text-text-primary'
|
||||
: 'border-transparent hover:text-text-primary'}"
|
||||
onclick={() => toggleSort(col.key)}
|
||||
>{col.label}{meta.sortBy === col.key
|
||||
? meta.sortDir === 'asc'
|
||||
? ' ↑'
|
||||
: ' ↓'
|
||||
: ''}</button
|
||||
>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each data as item, i}
|
||||
{@render row(item, i)}
|
||||
{:else}
|
||||
<p
|
||||
class="rounded-xl border border-dashed border-white/10 px-4 py-8 text-center text-sm text-text-secondary"
|
||||
>
|
||||
{meta.search ? `No results for "${meta.search}"` : 'Nothing here yet'}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- table -->
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-border bg-surface-800">
|
||||
{#each columns as col}
|
||||
<th
|
||||
class="{col.class ??
|
||||
'px-4 text-left'} py-3 text-[11px] font-semibold tracking-wider text-text-secondary uppercase"
|
||||
>
|
||||
{#if col.sortable}
|
||||
<button
|
||||
class="inline-flex items-center gap-1 transition-colors hover:text-text-primary"
|
||||
onclick={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.label}
|
||||
{#if meta.sortBy === col.key}
|
||||
<svg
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
>
|
||||
{#if meta.sortDir === 'asc'}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
|
||||
{:else}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
{/if}
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
class="h-3 w-3 opacity-30"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
{col.label}
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border/50">
|
||||
{#each data as item, i}
|
||||
{@render row(item, i)}
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan={columns.length} class="px-4 py-8 text-center text-sm text-text-secondary">
|
||||
{meta.search ? `No results for "${meta.search}"` : 'No data'}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- pagination -->
|
||||
{#if meta.totalPages > 1}
|
||||
|
||||
Reference in New Issue
Block a user