Initial
This commit is contained in:
100
client/src/components/ui/ResourceTable.vue
Normal file
100
client/src/components/ui/ResourceTable.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<article class="glass-panel resource-table-panel">
|
||||
<div class="resource-toolbar">
|
||||
<slot name="toolbar-leading" />
|
||||
<label class="search-control">
|
||||
<Search :size="15" />
|
||||
<input :value="search" :placeholder="searchPlaceholder" @input="$emit('update:search', $event.target.value)" />
|
||||
<button v-if="search" class="search-clear" type="button" aria-label="Clear search" @click="$emit('update:search', '')">
|
||||
<X :size="13" />
|
||||
</button>
|
||||
</label>
|
||||
<label class="table-density-select">
|
||||
<span>Rows</span>
|
||||
<select :value="pageSize" @change="$emit('update:pageSize', Number($event.target.value))">
|
||||
<option :value="8">8</option>
|
||||
<option :value="12">12</option>
|
||||
<option :value="20">20</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="toolbar-count">{{ total }} {{ itemLabel }}</span>
|
||||
<slot name="toolbar-actions" />
|
||||
</div>
|
||||
<div v-if="search || sort" class="resource-filter-strip">
|
||||
<span v-if="search">Search: <strong>{{ search }}</strong></span>
|
||||
<span v-if="sort">Sort: <strong>{{ activeSortLabel }}</strong> {{ direction }}</span>
|
||||
</div>
|
||||
<div v-if="rows.length" class="data-table-wrap">
|
||||
<table class="data-table resource-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in columns" :key="column.key">
|
||||
<button v-if="column.sortable !== false" type="button" :aria-sort="sortAria(column.key)" @click="$emit('sort', column.key)">
|
||||
{{ column.label }}
|
||||
<component :is="sortIcon(column.key)" :size="13" />
|
||||
</button>
|
||||
<template v-else>{{ column.label }}</template>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.id">
|
||||
<td v-for="column in columns" :key="column.key" :class="column.class">
|
||||
<slot :name="`cell-${column.key}`" :row="row" :value="row[column.key]">
|
||||
{{ row[column.key] || '-' }}
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="resource-empty-shell">
|
||||
<slot name="empty">
|
||||
<div class="widget-empty">{{ emptyText }}</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<button class="ghost-button compact" :disabled="page <= 1" aria-label="Previous page" @click="$emit('update:page', page - 1)">
|
||||
<ChevronLeft :size="15" />Previous
|
||||
</button>
|
||||
<span>Page {{ Math.min(page, pageCount) }} of {{ pageCount }}</span>
|
||||
<button class="ghost-button compact" :disabled="page >= pageCount" aria-label="Next page" @click="$emit('update:page', page + 1)">
|
||||
Next<ChevronRight :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ArrowDownAZ, ArrowUpAZ, ChevronsUpDown, ChevronLeft, ChevronRight, Search, X } from '@lucide/vue';
|
||||
|
||||
const props = defineProps({
|
||||
columns: { type: Array, required: true },
|
||||
rows: { type: Array, required: true },
|
||||
total: { type: Number, required: true },
|
||||
search: { type: String, default: '' },
|
||||
page: { type: Number, default: 1 },
|
||||
pageCount: { type: Number, default: 1 },
|
||||
pageSize: { type: Number, default: 8 },
|
||||
sort: { type: String, default: '' },
|
||||
direction: { type: String, default: 'asc' },
|
||||
searchPlaceholder: { type: String, default: 'Search...' },
|
||||
itemLabel: { type: String, default: 'items' },
|
||||
emptyText: { type: String, default: 'No rows match the current filters.' }
|
||||
});
|
||||
|
||||
defineEmits(['update:search', 'update:page', 'update:pageSize', 'sort']);
|
||||
|
||||
const activeSortLabel = computed(() => props.columns.find((column) => column.key === props.sort)?.label || props.sort);
|
||||
|
||||
function sortIcon(key) {
|
||||
if (props.sort !== key) return ChevronsUpDown;
|
||||
return props.direction === 'asc' ? ArrowUpAZ : ArrowDownAZ;
|
||||
}
|
||||
|
||||
function sortAria(key) {
|
||||
if (props.sort !== key) return 'none';
|
||||
return props.direction === 'asc' ? 'ascending' : 'descending';
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user