Files
BarangaySystem/resources/js/Components/Core/SearchableTableWrapper.vue
2026-06-06 18:43:00 +08:00

206 lines
4.6 KiB
Vue

<template>
<div class="searchable-table-wrapper">
<!-- Toolbar: Search + Density on same line -->
<div class="toolbar-row d-flex align-items-center gap-3 mb-4">
<div class="search-container flex-grow-1">
<SearchBar
v-model="searchModel"
:placeholder="searchPlaceholder"
/>
</div>
<div class="density-container flex-shrink-0">
<TableDensityToggle v-model="densityModel" />
</div>
</div>
<!-- Loading State -->
<div v-if="loading" class="mt-2">
<SkeletonTable :rows="skeletonRows" :columns="skeletonColumns" />
</div>
<!-- Error State -->
<div v-else-if="error" class="alert alert-danger">{{ error }}</div>
<!-- Empty State -->
<div v-else-if="empty" class="text-center py-5 no-results">
<slot name="empty-state">
<i :class="[emptyIcon, 'fa-4x text-muted opacity-25 mb-3']"></i>
<h5>{{ emptyTitle }}</h5>
<p class="text-muted">{{ emptyMessage }}</p>
</slot>
</div>
<!-- Table Content -->
<div v-else class="card border-0 shadow-sm rounded-20" :data-table-density="densityModel">
<div class="table-responsive">
<table class="table table-hover align-middle density-table mb-0" :class="tableDensityClass">
<slot name="table"></slot>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import SearchBar from './Search/SearchBar.vue'
import TableDensityToggle from './TableDensityToggle.vue'
import SkeletonTable from './Skeleton/SkeletonTable.vue'
const props = defineProps({
// Search
searchValue: { type: String, default: '' },
searchPlaceholder: { type: String, default: 'Search...' },
// Density
densityValue: { type: String, default: 'comfortable' },
// States
loading: { type: Boolean, default: false },
error: { type: String, default: null },
empty: { type: Boolean, default: false },
// Empty state customization
emptyIcon: { type: String, default: 'fas fa-inbox' },
emptyTitle: { type: String, default: 'No results found' },
emptyMessage: { type: String, default: 'Try adjusting your search criteria' },
// Skeleton config
skeletonRows: { type: Number, default: 8 },
skeletonColumns: { type: Number, default: 6 },
})
const emit = defineEmits(['update:searchValue', 'update:densityValue'])
const searchModel = computed({
get: () => props.searchValue,
set: (val) => emit('update:searchValue', val)
})
const densityModel = computed({
get: () => props.densityValue,
set: (val) => emit('update:densityValue', val)
})
const tableDensityClass = computed(() => {
return {
'density-comfortable': props.densityValue === 'comfortable',
'density-compact': props.densityValue === 'compact',
'density-ultra': props.densityValue === 'ultra-compact'
}
})
</script>
<style scoped>
.searchable-table-wrapper {
width: 100%;
}
.toolbar-row {
flex-wrap: nowrap;
}
.search-container {
min-width: 0;
flex: 1;
max-width: 50%;
}
.density-container {
flex: 1;
max-width: 50%;
}
.no-results {
background: var(--bg-card);
border-radius: 20px;
border: 2px dashed var(--border-color);
}
/* Table Density Styles */
.density-table :deep(thead th) {
transition: padding 0.2s ease;
}
.density-table :deep(tbody td) {
transition: padding 0.2s ease;
}
/* Comfortable - Default */
.density-comfortable :deep(thead th) {
padding: 1rem;
}
.density-comfortable :deep(tbody td) {
padding: 1rem;
}
/* Compact */
.density-compact :deep(thead th) {
padding: 0.625rem 0.75rem;
}
.density-compact :deep(tbody td) {
padding: 0.5rem 0.75rem;
}
.density-compact :deep(.store-thumb),
.density-compact :deep(.product-thumb) {
width: 36px !important;
height: 36px !important;
}
/* Ultra Compact */
.density-ultra :deep(thead th) {
padding: 0.375rem 0.5rem;
font-size: 0.7rem;
}
.density-ultra :deep(tbody td) {
padding: 0.25rem 0.5rem;
font-size: 0.8rem;
}
.density-ultra :deep(.store-thumb),
.density-ultra :deep(.product-thumb) {
width: 28px !important;
height: 28px !important;
}
.density-ultra :deep(.btn-icon) {
width: 26px !important;
height: 26px !important;
font-size: 0.7rem;
}
.density-ultra :deep(.small) {
font-size: 0.75rem;
}
.density-ultra :deep(.smallest) {
font-size: 0.65rem;
}
/* Responsive */
@media (max-width: 768px) {
.toolbar-row {
flex-wrap: wrap;
}
.search-container {
max-width: 100%;
width: 100%;
margin-bottom: 0.5rem;
}
.density-container {
width: 100%;
}
}
/* Dark Mode */
:global(.dark-mode) .no-results {
background: var(--bg-card) !important;
border-color: var(--border-color) !important;
}
</style>