Files
poshmanager/client/src/components/ui/ResourceTable.vue
2026-06-25 12:05:53 -05:00

101 lines
4.0 KiB
Vue

<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>