206 lines
4.6 KiB
Vue
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> |