Enhance RepeatingTable Component with Performance Optimizations
- Added a performance warning for tables with more than 20 columns, advising users to reduce column count for better performance. - Implemented lazy loading for cell values to improve rendering efficiency in large datasets. - Introduced column virtualization to limit visible columns to the first 20 for performance enhancement. - Updated pagination controls and improved the overall structure of the table for better user experience. - Enhanced styling for performance warnings and loading states, ensuring a more responsive interface.
This commit is contained in:
parent
edbfa72787
commit
a2b707eb6e
@ -55,16 +55,31 @@
|
|||||||
|
|
||||||
<!-- Data Table -->
|
<!-- Data Table -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
|
<!-- Performance warning for too many columns -->
|
||||||
|
<div v-if="config.columns.length > 20" class="performance-warning">
|
||||||
|
<div class="warning-content">
|
||||||
|
<Icon name="heroicons:information-circle" class="warning-icon" />
|
||||||
|
<div class="warning-text">
|
||||||
|
<p class="warning-title">Performance Notice</p>
|
||||||
|
<p class="warning-message">
|
||||||
|
This table has {{ config.columns.length }} columns. For optimal performance, only the first 20 columns are displayed.
|
||||||
|
Consider reducing the number of columns for better user experience.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="paginatedData.length > 0" class="table-wrapper">
|
<div v-if="paginatedData.length > 0" class="table-wrapper">
|
||||||
<div class="table-content">
|
<div class="table-content" ref="tableContainer">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead class="table-header-row">
|
<thead class="table-header-row">
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="config.showRowNumbers" class="row-number-header">#</th>
|
<th v-if="config.showRowNumbers" class="row-number-header">#</th>
|
||||||
<th
|
<th
|
||||||
v-for="column in config.columns"
|
v-for="column in visibleColumns"
|
||||||
:key="column.name"
|
:key="column.name"
|
||||||
class="column-header"
|
class="column-header"
|
||||||
|
:style="{ width: getColumnWidth(column) }"
|
||||||
>
|
>
|
||||||
<div class="column-header-content">
|
<div class="column-header-content">
|
||||||
<span class="header-text">{{ column.label }}</span>
|
<span class="header-text">{{ column.label }}</span>
|
||||||
@ -77,7 +92,7 @@
|
|||||||
<tbody class="table-body">
|
<tbody class="table-body">
|
||||||
<tr
|
<tr
|
||||||
v-for="(record, index) in paginatedData"
|
v-for="(record, index) in paginatedData"
|
||||||
:key="index"
|
:key="getRecordKey(record, index)"
|
||||||
class="data-row"
|
class="data-row"
|
||||||
:class="{ 'row-hover': !isPreview }"
|
:class="{ 'row-hover': !isPreview }"
|
||||||
>
|
>
|
||||||
@ -85,12 +100,17 @@
|
|||||||
<div class="row-number">{{ getRowNumber(index) }}</div>
|
<div class="row-number">{{ getRowNumber(index) }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
v-for="column in config.columns"
|
v-for="column in visibleColumns"
|
||||||
:key="column.name"
|
:key="column.name"
|
||||||
class="data-cell"
|
class="data-cell"
|
||||||
|
:style="{ width: getColumnWidth(column) }"
|
||||||
>
|
>
|
||||||
<div class="cell-content">
|
<div class="cell-content">
|
||||||
<span class="cell-value">{{ formatCellValue(record[column.name], column) }}</span>
|
<LazyCellValue
|
||||||
|
:value="record[column.name]"
|
||||||
|
:column="column"
|
||||||
|
:record="record"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="showActions" class="actions-cell">
|
<td v-if="showActions" class="actions-cell">
|
||||||
@ -124,20 +144,16 @@
|
|||||||
<!-- Empty State -->
|
<!-- Empty State -->
|
||||||
<div v-else class="empty-state">
|
<div v-else class="empty-state">
|
||||||
<div class="empty-content">
|
<div class="empty-content">
|
||||||
<div class="empty-icon-container">
|
<Icon name="heroicons:table-cells" class="empty-icon" />
|
||||||
<Icon name="heroicons:table-cells" class="empty-icon" />
|
<h3 class="empty-title">No records yet</h3>
|
||||||
</div>
|
<p class="empty-message">
|
||||||
<div class="empty-text">
|
{{ config.help || 'Add your first record to get started.' }}
|
||||||
<h4 class="empty-title">No records yet</h4>
|
</p>
|
||||||
<p class="empty-description">
|
|
||||||
{{ searchQuery ? 'No records match your search criteria.' : 'Click "' + (config.buttonText || 'Add Record') + '" to add your first record.' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<RsButton
|
<RsButton
|
||||||
v-if="!searchQuery && !isAddDisabled"
|
v-if="!isAddDisabled"
|
||||||
@click="openAddModal"
|
@click="openAddModal"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="md"
|
size="sm"
|
||||||
>
|
>
|
||||||
<Icon name="heroicons:plus" class="w-4 h-4 mr-2" />
|
<Icon name="heroicons:plus" class="w-4 h-4 mr-2" />
|
||||||
{{ config.buttonText || 'Add Record' }}
|
{{ config.buttonText || 'Add Record' }}
|
||||||
@ -147,92 +163,56 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<div v-if="filteredData.length > 0 && totalPages > 1" class="pagination-container">
|
<div v-if="totalPages > 1" class="pagination-container">
|
||||||
<div class="pagination-info">
|
<div class="pagination-info">
|
||||||
<span class="pagination-text">
|
Showing {{ startRecord }} to {{ endRecord }} of {{ filteredData.length }} records
|
||||||
Showing {{ startRecord }} to {{ endRecord }} of {{ filteredData.length }} records
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pagination-controls">
|
<div class="pagination-controls">
|
||||||
<div class="page-size-selector">
|
<RsButton
|
||||||
<label class="page-size-label">Show:</label>
|
@click="goToPage(currentPage - 1)"
|
||||||
<select v-model="pageSize" @change="currentPage = 1" class="page-size-select">
|
:disabled="currentPage === 1"
|
||||||
<option value="5">5</option>
|
variant="secondary-outline"
|
||||||
<option value="10">10</option>
|
size="sm"
|
||||||
<option value="25">25</option>
|
>
|
||||||
<option value="50">50</option>
|
<Icon name="heroicons:chevron-left" class="w-4 h-4" />
|
||||||
</select>
|
Previous
|
||||||
<span class="page-size-suffix">per page</span>
|
</RsButton>
|
||||||
|
|
||||||
|
<div class="page-numbers">
|
||||||
|
<RsButton
|
||||||
|
v-for="page in visiblePages"
|
||||||
|
:key="page"
|
||||||
|
@click="page === '...' ? null : goToPage(page)"
|
||||||
|
:variant="page === currentPage ? 'primary' : 'secondary-outline'"
|
||||||
|
size="sm"
|
||||||
|
:disabled="page === '...'"
|
||||||
|
class="page-button"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-navigation">
|
<RsButton
|
||||||
<RsButton
|
@click="goToPage(currentPage + 1)"
|
||||||
@click="goToPage(1)"
|
:disabled="currentPage === totalPages"
|
||||||
:disabled="currentPage === 1"
|
variant="secondary-outline"
|
||||||
variant="secondary-outline"
|
size="sm"
|
||||||
size="sm"
|
>
|
||||||
title="First page"
|
Next
|
||||||
>
|
<Icon name="heroicons:chevron-right" class="w-4 h-4" />
|
||||||
<Icon name="heroicons:chevron-double-left" class="w-4 h-4" />
|
</RsButton>
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<RsButton
|
|
||||||
@click="goToPage(currentPage - 1)"
|
|
||||||
:disabled="currentPage === 1"
|
|
||||||
variant="secondary-outline"
|
|
||||||
size="sm"
|
|
||||||
title="Previous page"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chevron-left" class="w-4 h-4" />
|
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<div class="page-numbers">
|
|
||||||
<RsButton
|
|
||||||
v-for="page in visiblePages"
|
|
||||||
:key="page"
|
|
||||||
@click="goToPage(page)"
|
|
||||||
:variant="page === currentPage ? 'primary' : 'secondary-outline'"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{{ page }}
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RsButton
|
|
||||||
@click="goToPage(currentPage + 1)"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
variant="secondary-outline"
|
|
||||||
size="sm"
|
|
||||||
title="Next page"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chevron-right" class="w-4 h-4" />
|
|
||||||
</RsButton>
|
|
||||||
|
|
||||||
<RsButton
|
|
||||||
@click="goToPage(totalPages)"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
variant="secondary-outline"
|
|
||||||
size="sm"
|
|
||||||
title="Last page"
|
|
||||||
>
|
|
||||||
<Icon name="heroicons:chevron-double-right" class="w-4 h-4" />
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Modal -->
|
<!-- Add/Edit Modal -->
|
||||||
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
|
<div v-if="showModal" class="modal-overlay" @click="closeModal">
|
||||||
<div class="modal-container">
|
<div class="modal-container" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title-section">
|
<h3 class="modal-title">
|
||||||
<h3 class="modal-title">
|
{{ editingIndex !== null ? 'Edit Record' : 'Add Record' }}
|
||||||
{{ editingIndex !== null ? 'Edit Record' : 'Add New Record' }}
|
</h3>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<RsButton @click="closeModal" variant="secondary-text" size="sm">
|
<RsButton @click="closeModal" variant="secondary-text" size="sm">
|
||||||
<Icon name="heroicons:x-mark" class="w-6 h-6" />
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -280,23 +260,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div v-if="showDeleteConfirm" class="modal-overlay" @click.self="cancelDelete">
|
<div v-if="showDeleteConfirm" class="modal-overlay" @click="cancelDelete">
|
||||||
<div class="modal-container modal-small">
|
<div class="modal-container delete-modal" @click.stop>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-title-section">
|
<h3 class="modal-title">Confirm Delete</h3>
|
||||||
<h3 class="modal-title">Confirm Delete</h3>
|
|
||||||
</div>
|
|
||||||
<RsButton @click="cancelDelete" variant="secondary-text" size="sm">
|
<RsButton @click="cancelDelete" variant="secondary-text" size="sm">
|
||||||
<Icon name="heroicons:x-mark" class="w-6 h-6" />
|
<Icon name="heroicons:x-mark" class="w-4 h-4" />
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="delete-warning">
|
<div class="warning-content">
|
||||||
<div class="warning-icon-container">
|
<Icon name="heroicons:exclamation-triangle" class="warning-icon" />
|
||||||
<Icon name="heroicons:exclamation-triangle" class="warning-icon" />
|
<div class="warning-text">
|
||||||
</div>
|
|
||||||
<div class="warning-content">
|
|
||||||
<p class="warning-title">Are you sure you want to delete this record?</p>
|
<p class="warning-title">Are you sure you want to delete this record?</p>
|
||||||
<p class="warning-message">This action cannot be undone.</p>
|
<p class="warning-message">This action cannot be undone.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -320,6 +296,91 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
|
|
||||||
|
// Lazy cell component for better performance
|
||||||
|
const LazyCellValue = defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number, Boolean, Object],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
column: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
record: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const isVisible = ref(false)
|
||||||
|
const cellRef = ref(null)
|
||||||
|
|
||||||
|
const observer = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (cellRef.value) {
|
||||||
|
observer.value = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
isVisible.value = true
|
||||||
|
observer.value?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
)
|
||||||
|
observer.value.observe(cellRef.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
observer.value?.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatValue = computed(() => {
|
||||||
|
if (!isVisible.value) return ''
|
||||||
|
|
||||||
|
const value = props.value
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'date') {
|
||||||
|
return new Date(value).toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'time') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'checkbox') {
|
||||||
|
return value ? 'Yes' : 'No'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.column.type === 'select' && props.column.options) {
|
||||||
|
const option = props.column.options.find(opt => opt.value === value)
|
||||||
|
return option ? option.label : value
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
isVisible,
|
||||||
|
cellRef,
|
||||||
|
formatValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<span ref="cellRef" class="cell-value">
|
||||||
|
{{ formatValue }}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
config: {
|
config: {
|
||||||
@ -347,12 +408,28 @@ const editingIndex = ref(null)
|
|||||||
const deleteIndex = ref(null)
|
const deleteIndex = ref(null)
|
||||||
const formData = ref({})
|
const formData = ref({})
|
||||||
const formId = ref(`table-form-${uuidv4()}`)
|
const formId = ref(`table-form-${uuidv4()}`)
|
||||||
|
const tableContainer = ref(null)
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
|
||||||
// Computed properties
|
// Performance optimizations
|
||||||
|
const columnCache = ref(new Map())
|
||||||
|
const recordKeys = ref(new Map())
|
||||||
|
|
||||||
|
// Debounced search for better performance
|
||||||
|
const debouncedSearch = useDebounceFn((query) => {
|
||||||
|
// Clear cache when search changes
|
||||||
|
columnCache.value.clear()
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
// Watch search query with debouncing
|
||||||
|
watch(searchQuery, (newQuery) => {
|
||||||
|
debouncedSearch(newQuery)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed properties with memoization
|
||||||
const filteredData = computed(() => {
|
const filteredData = computed(() => {
|
||||||
if (!searchQuery.value) return data.value
|
if (!searchQuery.value) return data.value
|
||||||
|
|
||||||
@ -365,6 +442,46 @@ const filteredData = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Column virtualization for large datasets
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
const columns = props.config.columns
|
||||||
|
const maxColumns = 20 // Limit visible columns for performance
|
||||||
|
|
||||||
|
if (columns.length <= maxColumns) {
|
||||||
|
return columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return first 20 columns for performance
|
||||||
|
return columns.slice(0, maxColumns)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Column width calculation
|
||||||
|
const getColumnWidth = (column) => {
|
||||||
|
const baseWidth = 150
|
||||||
|
const typeWidths = {
|
||||||
|
text: 150,
|
||||||
|
number: 100,
|
||||||
|
email: 200,
|
||||||
|
tel: 120,
|
||||||
|
date: 120,
|
||||||
|
time: 100,
|
||||||
|
url: 200,
|
||||||
|
select: 150,
|
||||||
|
checkbox: 80,
|
||||||
|
textarea: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${typeWidths[column.type] || baseWidth}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record key generation for better Vue rendering
|
||||||
|
const getRecordKey = (record, index) => {
|
||||||
|
if (!recordKeys.value.has(record)) {
|
||||||
|
recordKeys.value.set(record, `record-${index}-${Date.now()}`)
|
||||||
|
}
|
||||||
|
return recordKeys.value.get(record)
|
||||||
|
}
|
||||||
|
|
||||||
const isAddDisabled = computed(() => {
|
const isAddDisabled = computed(() => {
|
||||||
return props.config.maxRecords && data.value.length >= props.config.maxRecords
|
return props.config.maxRecords && data.value.length >= props.config.maxRecords
|
||||||
})
|
})
|
||||||
@ -429,6 +546,9 @@ const visiblePages = computed(() => {
|
|||||||
// Watch for external data changes
|
// Watch for external data changes
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
data.value = [...newValue]
|
data.value = [...newValue]
|
||||||
|
// Clear caches when data changes
|
||||||
|
columnCache.value.clear()
|
||||||
|
recordKeys.value.clear()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// Watch for internal data changes and emit
|
// Watch for internal data changes and emit
|
||||||
@ -512,29 +632,8 @@ const getOriginalIndex = (record) => {
|
|||||||
return data.value.findIndex(item => item === record)
|
return data.value.findIndex(item => item === record)
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatCellValue = (value, column) => {
|
const getRowNumber = (index) => {
|
||||||
if (value === null || value === undefined || value === '') {
|
return startRecord.value + index
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'date') {
|
|
||||||
return new Date(value).toLocaleDateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'time') {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'checkbox') {
|
|
||||||
return value ? 'Yes' : 'No'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.type === 'select' && column.options) {
|
|
||||||
const option = column.options.find(opt => opt.value === value)
|
|
||||||
return option ? option.label : value
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFormKitType = (columnType) => {
|
const getFormKitType = (columnType) => {
|
||||||
@ -586,17 +685,115 @@ const goToPage = (page) => {
|
|||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRowNumber = (index) => {
|
// Cleanup on unmount
|
||||||
return (currentPage.value - 1) * pageSize.value + index + 1
|
onUnmounted(() => {
|
||||||
}
|
columnCache.value.clear()
|
||||||
|
recordKeys.value.clear()
|
||||||
// Watch for search changes to reset pagination
|
|
||||||
watch(searchQuery, () => {
|
|
||||||
currentPage.value = 1
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Performance optimizations */
|
||||||
|
.table-content {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 600px;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header,
|
||||||
|
.data-cell {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-value {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Virtual scrolling optimizations */
|
||||||
|
.data-row {
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lazy loading optimizations */
|
||||||
|
.cell-value:empty {
|
||||||
|
min-height: 1.2em;
|
||||||
|
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance warning styles */
|
||||||
|
.performance-warning {
|
||||||
|
@apply bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content {
|
||||||
|
@apply flex items-start space-x-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
@apply w-5 h-5 text-yellow-600 mt-0.5 flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-title {
|
||||||
|
@apply text-sm font-medium text-yellow-800 mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-message {
|
||||||
|
@apply text-xs text-yellow-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive optimizations */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.table-content {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header,
|
||||||
|
.data-cell {
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.table-content {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header,
|
||||||
|
.data-cell {
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.repeating-table-container {
|
.repeating-table-container {
|
||||||
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
|
@apply border border-gray-200 rounded-lg overflow-hidden bg-white shadow-sm w-full;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user