diff --git a/resources/js/pages/dashboards/demo.vue b/resources/js/pages/dashboards/demo.vue index 82c2a22..a04da55 100644 --- a/resources/js/pages/dashboards/demo.vue +++ b/resources/js/pages/dashboards/demo.vue @@ -8,25 +8,55 @@ import CostOverview from "@/components/CostOverview.vue"; import GeneratedLeadsCard from "@/views/dashboards/ecommerce/EcommerceGeneratedLeads.vue"; import AgGridTable from '@/views/demos/forms/tables/data-table/AgGridTable.vue'; -const ganttColumns = ref([ - { field: 'id', headerName: 'ID', sortable: true, filter: true, width: 80 }, - { field: 'todo', headerName: 'Task', sortable: true, filter: true, flex: 1 }, - { field: 'userId', headerName: 'User ID', sortable: true, filter: true, width: 100 }, - { - field: 'completed', headerName: 'Status', sortable: true, filter: true, width: 120, cellRenderer: (params) => { - return params.value ? '✓ Completed' : '⏳ Pending' +const ganttColumns = ref([]) + +const generateColumnsFromData = (data) => { + if (!data || data.length === 0) return [] + + const firstItem = data[0] + const columns = Object.keys(firstItem).map(key => { + const column = { + field: key, + headerName: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'), + sortable: true, + filter: true, + flex: 1 } - } -]) + + if (key === 'id') { + column.width = 80 + column.flex = undefined + } + + if (key === 'completed' && typeof firstItem[key] === 'boolean') { + column.width = 120 + column.flex = undefined + column.cellRenderer = (params) => { + return params.value ? '✓ Completed' : '⏳ Pending' + } + } + + if (key === 'userId') { + column.width = 100 + column.flex = undefined + } + + return column + }) + + return columns +} const processAPIData = (response) => { - return response.todos || response + const data = response.todos || response + ganttColumns.value = generateColumnsFromData(data) + return data } onMounted(async () => { const grid = GridStack.init({ column: 12, - cellHeight: '110', + cellHeight: '105', float: true, draggable: { handle: '.grid-stack-item-content' }, resizable: true, @@ -39,7 +69,7 @@ onMounted(async () => { - - - \ No newline at end of file diff --git a/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue b/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue index 3d8e960..ba12524 100644 --- a/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue +++ b/resources/js/views/demos/forms/tables/data-table/AgGridTable.vue @@ -14,75 +14,83 @@ import autoTable from 'jspdf-autotable' ModuleRegistry.registerModules([AllCommunityModule]) const props = defineProps({ - apiUrl: { - type: String, - default: '' - }, - apiHeaders: { - type: Object, - default: () => ({}) - }, - data: { - type: Array, - default: () => [] - }, - columns: { - type: Array, - default: () => [] - }, - height: { - type: String, - default: '600px' - }, - mobileHeight: { - type: String, - default: '400px' - }, - enableExport: { - type: Boolean, - default: true - }, - enableEdit: { - type: Boolean, - default: true - }, - enableDelete: { - type: Boolean, - default: true - }, - enableSearch: { - type: Boolean, - default: true - }, - loading: { - type: Boolean, - default: false - }, - customActions: { - type: Array, - default: () => [] - }, - dataProcessor: { - type: Function, - default: null - }, - schemaMap: { - type: Object, - default: () => ({}) - }, - keepOriginalKeys: { - type: Boolean, - default: false - } + apiUrl: { + type: String, + default: '' + }, + apiHeaders: { + type: Object, + default: () => ({}) + }, + data: { + type: Array, + default: () => [] + }, + columns: { + type: Array, + default: () => [] + }, + height: { + type: String, + default: '600px' + }, + mobileHeight: { + type: String, + default: '400px' + }, + enableExport: { + type: Boolean, + default: true + }, + enableEdit: { + type: Boolean, + default: true + }, + enableDelete: { + type: Boolean, + default: true + }, + enableSearch: { + type: Boolean, + default: true + }, + loading: { + type: Boolean, + default: false + }, + customActions: { + type: Array, + default: () => [] + }, + dataProcessor: { + type: Function, + default: null + }, + schemaMap: { + type: Object, + default: () => ({}) + }, + keepOriginalKeys: { + type: Boolean, + default: false + }, + maxCellTextLength: { + type: Number, + default: 30 + }, + tableId: { + type: String, + default: 'default' + } }) const emit = defineEmits([ - 'data-updated', - 'row-selected', - 'row-deleted', - 'row-edited', - 'export-completed', - 'api-error' + 'data-updated', + 'row-selected', + 'row-deleted', + 'row-edited', + 'export-completed', + 'api-error' ]) const { isMobile, isTablet, windowWidth } = useResponsive() @@ -127,13 +135,42 @@ const processedData = computed(() => { }) }) +// Custom cell renderer component for long text with tooltip +const LongTextCellRenderer = { + template: ` +
+ {{ displayText }} +
+ `, + setup(props) { + const fullText = props.value || '' + const maxLength = props.colDef?.cellRendererParams?.maxLength || 30 + const displayText = fullText.length > maxLength + ? fullText.substring(0, maxLength) + '...' + : fullText + + return { + fullText, + displayText + } + } +} + // Dynamically infer columns from data when not provided const inferredColumns = computed(() => { if (Array.isArray(props.columns) && props.columns.length) return props.columns const rows = Array.isArray(processedData.value) ? processedData.value.slice(0, 50) : [] if (!rows.length) return [] - // Collect union of keys across sample rows const keySet = new Set() rows.forEach(r => { if (r && typeof r === 'object') { @@ -143,7 +180,7 @@ const inferredColumns = computed(() => { } }) - const keys = Array.from(keySet) + const keys = Array.from(keySet).filter(k => k !== 'action') const toTitle = (k) => k .replace(/_/g, ' ') .replace(/([a-z])([A-Z])/g, '$1 $2') @@ -151,20 +188,28 @@ const inferredColumns = computed(() => { const isLikelyDate = (v) => { if (typeof v !== 'string') return false - // ISO or yyyy-mm-dd or timestamps in string if (/^\d{4}-\d{2}-\d{2}/.test(v) || /T\d{2}:\d{2}:\d{2}/.test(v)) return true const d = new Date(v) return !isNaN(d.getTime()) } - return keys.map(k => { - // Detect type by scanning a few rows - let sampleVal = undefined - for (let i = 0; i < rows.length; i++) { + const isLongText = (samples) => { + return samples.some(val => + typeof val === 'string' && val.length > props.maxCellTextLength + ) + } + + const columns = keys.map(k => { + const sampleValues = [] + for (let i = 0; i < Math.min(10, rows.length); i++) { const val = rows[i]?.[k] - if (val !== undefined && val !== null) { sampleVal = val; break } + if (val !== undefined && val !== null) { + sampleValues.push(val) + } } + const sampleVal = sampleValues[0] + const col = { field: k, headerName: toTitle(k), @@ -173,6 +218,7 @@ const inferredColumns = computed(() => { editable: false, minWidth: 100, flex: 1, + suppressMovable: true } if (typeof sampleVal === 'number') { @@ -189,16 +235,42 @@ const inferredColumns = computed(() => { const d = new Date(p.value) return isNaN(d.getTime()) ? String(p.value) : d.toISOString().split('T')[0] } + } else if (typeof sampleVal === 'string' && isLongText(sampleValues)) { + col.flex = 1 + col.cellRenderer = LongTextCellRenderer + col.cellRendererParams = { + maxLength: props.maxCellTextLength + } + col.tooltipField = k } else { col.flex = 1 } + return col }) + + columns.push({ + headerName: "ACTION", + field: "action", + sortable: false, + filter: false, + flex: 1, + minWidth: 100, + hide: false, + editable: false, + cellRenderer: actionButtonsRenderer, + suppressHeaderMenuButton: true, + suppressMovable: true, + lockPosition: true, + colId: "action" + }) + + return columns }) -// Build grid with inferred columns (or props.columns if provided) -// Note: Passing inferredColumns.value at setup time; if you need live re-init on schema change, we can add a watcher. -let gridSetup = useDataTableGrid(processedData, isMobile, isTablet, inferredColumns) + +let gridSetup = useDataTableGrid(processedData, isMobile, isTablet, inferredColumns, props.tableId) +console.log('AgGridTable props.tableId:', props.tableId); const gridApi = gridSetup.gridApi const columnDefs = gridSetup.columnDefs const rowData = gridSetup.rowData @@ -228,344 +300,339 @@ watch( ) const fetchData = async () => { - if (!props.apiUrl) return - - isLoading.value = true - try { - const response = await fetch(props.apiUrl, { - headers: { - 'Content-Type': 'application/json', - ...props.apiHeaders - } - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - const result = await response.json() - - let processedApiData = [] - if (props.dataProcessor && typeof props.dataProcessor === 'function') { - processedApiData = props.dataProcessor(result) - } else { - processedApiData = result.todos || result.data || result.users || result.products || (Array.isArray(result) ? result : []) - } - - fetchedData.value = Array.isArray(processedApiData) ? processedApiData : [] - console.log('Fetched data set to:', fetchedData.value) - emit('data-updated', fetchedData.value) - } catch (error) { - console.error('Error fetching data:', error) - emit('api-error', error) - } finally { - isLoading.value = false - } + if (!props.apiUrl) return + + isLoading.value = true + try { + const response = await fetch(props.apiUrl, { + headers: { + 'Content-Type': 'application/json', + ...props.apiHeaders + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const result = await response.json() + + let processedApiData = [] + if (props.dataProcessor && typeof props.dataProcessor === 'function') { + processedApiData = props.dataProcessor(result) + } else { + processedApiData = result.todos || result.data || result.users || result.products || (Array.isArray(result) ? result : []) + } + + fetchedData.value = Array.isArray(processedApiData) ? processedApiData : [] + console.log('Fetched data set to:', fetchedData.value) + emit('data-updated', fetchedData.value) + } catch (error) { + console.error('Error fetching data:', error) + emit('api-error', error) + } finally { + isLoading.value = false + } } const saveToAPI = async (userData, isUpdate = false) => { - if (!props.apiUrl) return - - try { - const method = isUpdate ? 'PUT' : 'POST' - const url = isUpdate ? `${props.apiUrl}/${userData.id}` : props.apiUrl - - const response = await fetch(url, { - method, - headers: { - 'Content-Type': 'application/json', - ...props.apiHeaders - }, - body: JSON.stringify(userData) - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - await fetchData() - return await response.json() - } catch (error) { - console.error('Error saving data:', error) - emit('api-error', error) - throw error - } + if (!props.apiUrl) return + + try { + const method = isUpdate ? 'PUT' : 'POST' + const url = isUpdate ? `${props.apiUrl}/${userData.id}` : props.apiUrl + + const response = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + ...props.apiHeaders + }, + body: JSON.stringify(userData) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + await fetchData() + return await response.json() + } catch (error) { + console.error('Error saving data:', error) + emit('api-error', error) + throw error + } } const deleteFromAPI = async (id) => { - if (!props.apiUrl) return - - try { - const response = await fetch(`${props.apiUrl}/${id}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - ...props.apiHeaders - } - }) - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - - await fetchData() - return true - } catch (error) { - console.error('Error deleting data:', error) - emit('api-error', error) - throw error - } + if (!props.apiUrl) return + + try { + const response = await fetch(`${props.apiUrl}/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + ...props.apiHeaders + } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + await fetchData() + return true + } catch (error) { + console.error('Error deleting data:', error) + emit('api-error', error) + throw error + } } const { - setupGlobalHandlers, - cleanupGlobalHandlers, - exportToCSV, - exportToExcel, - saveUser, - confirmDelete, - cancelDelete, - showDetailsDialog, - selectedRowData, - deleteRowData, - showDeleteDialog, - deleteRow -} = useDataTableActions(gridApi, rowData) + setupGlobalHandlers, + cleanupGlobalHandlers, + exportToCSV, + exportToExcel, + saveUser, + confirmDelete, + cancelDelete, + showDetailsDialog, + selectedRowData, + deleteRowData, + showDeleteDialog, + deleteRow +} = useDataTableActions(gridApi, rowData, props.tableId) const handleSaveUser = async (updatedData) => { -try { - // Optimistic update: update grid first - saveUser(updatedData) + try { + // Optimistic update: update grid first + saveUser(updatedData) - if (props.apiUrl) { - try { - await saveToAPI(updatedData, updatedData.id ? true : false) - } catch (apiErr) { - console.error('API save failed, refetching to sync:', apiErr) - // Re-sync from server on failure - await fetchData() - emit('api-error', apiErr) - return + if (props.apiUrl) { + try { + await saveToAPI(updatedData, updatedData.id ? true : false) + } catch (apiErr) { + console.error('API save failed, refetching to sync:', apiErr) + // Re-sync from server on failure + await fetchData() + emit('api-error', apiErr) + return + } } + emit('row-edited', updatedData) + } catch (error) { + console.error('Error saving user:', error) } - emit('row-edited', updatedData) -} catch (error) { - console.error('Error saving user:', error) -} } const handleConfirmDelete = async () => { -try { - // Optimistic remove from grid first - confirmDelete() + try { + // Optimistic remove from grid first + confirmDelete() - if (props.apiUrl && deleteRowData.value?.id) { - try { - await deleteFromAPI(deleteRowData.value.id) - } catch (apiErr) { - console.error('API delete failed, refetching to restore:', apiErr) - await fetchData() - emit('api-error', apiErr) - return + if (props.apiUrl && deleteRowData.value?.id) { + try { + await deleteFromAPI(deleteRowData.value.id) + } catch (apiErr) { + console.error('API delete failed, refetching to restore:', apiErr) + await fetchData() + emit('api-error', apiErr) + return + } } + emit('row-deleted', deleteRowData.value) + } catch (error) { + console.error('Error deleting row:', error) } - emit('row-deleted', deleteRowData.value) -} catch (error) { - console.error('Error deleting row:', error) -} } const quickFilter = ref('') watch(quickFilter, (newValue) => { - if (gridApi.value) { - gridApi.value.setGridOption('quickFilterText', newValue || '') - } + if (gridApi.value) { + gridApi.value.setGridOption('quickFilterText', newValue || '') + } }, { immediate: false }) const onGridReady = params => { - gridReady(params) - setupGlobalHandlers() - - if (props.apiUrl) { - fetchData() - } + gridReady(params) + setupGlobalHandlers() - // Observe container size to recalc pagination - try { - const container = gridContainer?.value - if (container && typeof ResizeObserver !== 'undefined') { - const ro = new ResizeObserver(() => { - if (gridApi.value) { - updatePagination() - setTimeout(() => { - gridApi.value?.sizeColumnsToFit?.() - }, 50) - } - }) - ro.observe(container) - } - } catch (e) { - // no-op - } + setTimeout(() => { + const allPageSizeWrappers = document.querySelectorAll('.ag-paging-page-size .ag-picker-field-wrapper'); + allPageSizeWrappers.forEach((pageSizeWrapper, index) => { + if (!pageSizeWrapper.hasAttribute('data-fixed')) { + pageSizeWrapper.setAttribute('data-fixed', 'true'); + + pageSizeWrapper.addEventListener('mousedown', (e) => { + if (e.button !== 0) { + e.preventDefault(); + e.stopPropagation(); + return; + } + e.preventDefault(); + e.stopPropagation(); + const isExpanded = pageSizeWrapper.getAttribute('aria-expanded') === 'true'; + pageSizeWrapper.setAttribute('aria-expanded', !isExpanded); + pageSizeWrapper.classList.toggle('ag-picker-collapsed', isExpanded); + pageSizeWrapper.classList.toggle('ag-picker-expanded', !isExpanded); + const selectList = pageSizeWrapper.parentNode.querySelector('[id^="ag-select-list"]'); + if (selectList) { + selectList.style.display = isExpanded ? 'none' : 'block'; + } + }); + + pageSizeWrapper.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + } + }); + }, 1000); + + if (props.apiUrl) { + fetchData() + } + + try { + const container = gridContainer?.value + if (container && typeof ResizeObserver !== 'undefined') { + const ro = new ResizeObserver(() => { + if (gridApi.value) { + updatePagination() + setTimeout(() => { + gridApi.value?.sizeColumnsToFit?.() + }, 50) + } + }) + ro.observe(container) + } + } catch (e) { } } const handleQuickFilterUpdate = (value) => { - quickFilter.value = value || '' - - if (gridApi.value) { - gridApi.value.setGridOption('quickFilterText', value || '') - } + quickFilter.value = value || '' + + if (gridApi.value) { + gridApi.value.setGridOption('quickFilterText', value || '') + } } const refreshData = () => { - if (props.apiUrl) { - fetchData() - } + if (props.apiUrl) { + fetchData() + } } defineExpose({ - refreshData, - fetchData, - gridApi + refreshData, + fetchData, + gridApi }) onMounted(() => { - setupGlobalHandlers() - window.addEventListener('resize', handleResize) + setupGlobalHandlers() + window.addEventListener('resize', handleResize) }) onUnmounted(() => { - cleanupGlobalHandlers() - window.removeEventListener('resize', handleResize) + cleanupGlobalHandlers() + window.removeEventListener('resize', handleResize) }) const handleResize = () => { - windowWidth.value = window.innerWidth - if (gridApi.value) { - updateColumnVisibility() - updatePagination() - setTimeout(() => { - if (gridApi.value && gridApi.value.sizeColumnsToFit) { - gridApi.value.sizeColumnsToFit() - } - }, 100) - } + windowWidth.value = window.innerWidth + if (gridApi.value) { + updateColumnVisibility() + updatePagination() + setTimeout(() => { + if (gridApi.value) { + gridApi.value.sizeColumnsToFit(); + } + }, 100) + } } -const exportToPDF = () => { - if (!gridApi.value) { - console.error('Grid API not available') - return - } - try { - const doc = new jsPDF() - const rowDataForPDF = [] - const headers = [] - - columnDefs.value.forEach(col => { - if (col.headerName && col.field && col.field !== 'action') { - headers.push(col.headerName) - } - }) - - gridApi.value.forEachNodeAfterFilterAndSort(node => { - const row = [] - columnDefs.value.forEach(col => { - if (col.field && col.field !== 'action') { - let value = node.data[col.field] || '' - row.push(value) - } - }) - rowDataForPDF.push(row) - }) - - autoTable(doc, { - head: [headers], - body: rowDataForPDF, - styles: { - fontSize: 8, - cellPadding: 2 - }, - headStyles: { - fillColor: [41, 128, 185], - textColor: 255, - fontStyle: 'bold' - }, - alternateRowStyles: { - fillColor: [245, 245, 245] - }, - margin: { top: 20 } - }) - - doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`) - emit('export-completed', 'pdf') - } catch (error) { - console.error('Error exporting PDF:', error) - } +const exportToPDF = () => { + if (!gridApi.value) { + console.error('Grid API not available') + return + } + + try { + const doc = new jsPDF() + const rowDataForPDF = [] + const headers = [] + + columnDefs.value.forEach(col => { + if (col.headerName && col.field && col.field !== 'action') { + headers.push(col.headerName) + } + }) + + gridApi.value.forEachNodeAfterFilterAndSort(node => { + const row = [] + columnDefs.value.forEach(col => { + if (col.field && col.field !== 'action') { + let value = node.data[col.field] || '' + row.push(value) + } + }) + rowDataForPDF.push(row) + }) + + autoTable(doc, { + head: [headers], + body: rowDataForPDF, + styles: { + fontSize: 8, + cellPadding: 2 + }, + headStyles: { + fillColor: [41, 128, 185], + textColor: 255, + fontStyle: 'bold' + }, + alternateRowStyles: { + fillColor: [245, 245, 245] + }, + margin: { top: 20 } + }) + + doc.save(`data-table-${new Date().toISOString().split('T')[0]}.pdf`) + emit('export-completed', 'pdf') + } catch (error) { + console.error('Error exporting PDF:', error) + } } \ No newline at end of file + + + diff --git a/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js b/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js index 1a205a9..1b2604d 100644 --- a/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js +++ b/resources/js/views/demos/forms/tables/data-table/composables/useDataTableGrid.js @@ -470,40 +470,42 @@ export function useDataTableGrid( return [] } - const processCustomColumns = (columns) => { - if (!Array.isArray(columns) || columns.length === 0) { - const base = createDefaultColumns(); - return ensureSingleSelectionColumn(base); - } + const processCustomColumns = (columns) => { + if (!Array.isArray(columns) || columns.length === 0) { + const base = createDefaultColumns(); + return ensureSingleSelectionColumn(base); + } - const processedColumns = columns.map(col => ({ - ...col, - // Default to inline-editable unless explicitly disabled, skip action column - editable: col.field === 'action' ? false : (col.editable !== undefined ? col.editable : true), - suppressHeaderMenuButton: col.suppressHeaderMenuButton !== undefined ? col.suppressHeaderMenuButton : false, - })); + const processedColumns = columns.map(col => ({ + ...col, + editable: col.field === 'action' ? false : (col.editable !== undefined ? col.editable : true), + suppressHeaderMenuButton: col.suppressHeaderMenuButton !== undefined ? col.suppressHeaderMenuButton : false, + })); - const withSelection = ensureSingleSelectionColumn(processedColumns); + const withSelection = ensureSingleSelectionColumn(processedColumns); + + const actionIndex = withSelection.findIndex(col => col.field === 'action'); + + if (actionIndex !== -1) { + const actionColumn = withSelection.splice(actionIndex, 1)[0]; + withSelection.push(actionColumn); + } else { + withSelection.push({ + headerName: "ACTION", + field: "action", + sortable: false, + filter: false, + flex: 1, + minWidth: 100, + hide: false, + editable: false, + cellRenderer: actionButtonsRenderer, + suppressHeaderMenuButton: true, + }); + } - const hasActionColumn = processedColumns.some(col => col.field === 'action'); - - if (!hasActionColumn) { - withSelection.push({ - headerName: "ACTION", - field: "action", - sortable: false, - filter: false, - flex: 1, - minWidth: 100, - hide: false, - editable: false, - cellRenderer: actionButtonsRenderer, - suppressHeaderMenuButton: true, - }); - } - - return withSelection; - }; + return withSelection; +}; const columnDefs = computed(() => { const cols = resolveColumns() @@ -529,6 +531,7 @@ export function useDataTableGrid( suppressHeaderMenuButton: false, })); + // 🔥 FIX: Improved gridOptions with better pagination settings const gridOptions = computed(() => ({ theme: "legacy", headerHeight: isMobile.value ? 48 : 56, @@ -541,9 +544,14 @@ export function useDataTableGrid( rowSelection: 'multiple', rowMultiSelectWithClick: true, suppressRowClickSelection: false, + + // 🔥 FIXED PAGINATION SETTINGS pagination: true, - paginationPageSize: isMobile.value ? 5 : 10, - paginationPageSizeSelector: isMobile.value ? [5, 10, 20] : [5, 10, 20, 50], + paginationPageSize: isMobile.value ? 10 : 20, + paginationPageSizeSelector: [5, 10, 20, 50, 100], + paginationAutoPageSize: false, // 🔥 KEY: Disable auto page sizing + suppressPaginationPanel: false, // 🔥 KEY: Show pagination controls + enableCellTextSelection: true, suppressHorizontalScroll: false, alwaysShowHorizontalScroll: false, @@ -597,29 +605,18 @@ export function useDataTableGrid( } }; + // 🔥 SIMPLIFIED UPDATE PAGINATION - Only for responsive page sizes const updatePagination = () => { if (!gridApi.value) return; try { - const gui = gridApi.value.getGui && gridApi.value.getGui(); - const containerHeight = gui ? gui.clientHeight : 0; - - const headerH = Number(gridOptions.value.headerHeight || (isMobile.value ? 48 : 56)); - // Estimate footer height based on overrides (mobile/desktop) - const footerH = isMobile.value ? 48 : 56; - const rowH = Number(gridOptions.value.rowHeight || (isMobile.value ? 44 : 52)); - const padding = 8; - - let available = containerHeight - headerH - footerH - padding; - if (!Number.isFinite(available) || available <= 0) { - available = (isMobile.value ? 360 : 520) - headerH - footerH - padding; - } - - let rowsPerPage = Math.floor(available / rowH); - rowsPerPage = Math.max(3, Math.min(rowsPerPage, 100)); - + // Simple responsive pagination size adjustment + const newPageSize = isMobile.value ? 10 : 20; + if (typeof gridApi.value.paginationSetPageSize === 'function') { - gridApi.value.paginationSetPageSize(rowsPerPage); + gridApi.value.paginationSetPageSize(newPageSize); + } else if (typeof gridApi.value.setGridOption === 'function') { + gridApi.value.setGridOption('paginationPageSize', newPageSize); } } catch (error) { console.warn('Error updating pagination:', error); @@ -632,7 +629,7 @@ export function useDataTableGrid( console.log('Grid API set:', gridApi.value) updateColumnVisibility(); - updatePagination(); + // 🔥 FIX: Don't call updatePagination immediately - let AG Grid handle it setTimeout(() => { if (gridApi.value && gridApi.value.sizeColumnsToFit) { @@ -664,7 +661,7 @@ export function useDataTableGrid( watch([isMobile, isTablet], () => { updateColumnVisibility(); - updatePagination(); + updatePagination(); // Only for responsive size changes }); watch(data, (newData) => { @@ -687,7 +684,6 @@ export function useDataTableGrid( } // Helpers - function ensureSingleSelectionColumn(columns) { if (!Array.isArray(columns)) return columns; @@ -714,6 +710,4 @@ function ensureSingleSelectionColumn(columns) { } return unique; -} - -// removed selection column injection \ No newline at end of file +} \ No newline at end of file diff --git a/resources/styles/ag-grid-overrides.scss b/resources/styles/ag-grid-overrides.scss index 9a926df..2e8e340 100644 --- a/resources/styles/ag-grid-overrides.scss +++ b/resources/styles/ag-grid-overrides.scss @@ -607,31 +607,7 @@ justify-content: space-between !important; transition: all 0.2s ease-in-out !important; position: relative !important; - overflow: visible !important; -} - -/* Ensure page-size picker can overflow and is clickable */ -.ag-theme-alpine-dark.vuetify-grid .ag-paging-page-size, -.ag-theme-alpine.vuetify-grid .ag-paging-page-size { - position: relative !important; - overflow: visible !important; - z-index: 2 !important; -} - -.ag-theme-alpine-dark.vuetify-grid .ag-wrapper.ag-picker-field-wrapper, -.ag-theme-alpine.vuetify-grid .ag-wrapper.ag-picker-field-wrapper { - position: relative !important; - pointer-events: auto !important; -} - -/* Raise ag-Grid popups above footer */ -.ag-theme-alpine-dark.vuetify-grid .ag-popup, -.ag-theme-alpine.vuetify-grid .ag-popup, -.ag-theme-alpine-dark.vuetify-grid .ag-select-list, -.ag-theme-alpine.vuetify-grid .ag-select-list, -.ag-theme-alpine-dark.vuetify-grid .ag-menu, -.ag-theme-alpine.vuetify-grid .ag-menu { - z-index: 9999 !important; + overflow: hidden !important; } .ag-theme-alpine-dark.vuetify-grid .ag-paging-panel:hover { @@ -971,7 +947,7 @@ justify-content: space-between !important; transition: all 0.2s ease-in-out !important; position: relative !important; - overflow: visible !important; + overflow: hidden !important; font-size: 14px !important; } diff --git a/resources/styles/gantt-chart.scss b/resources/styles/gantt-chart.scss new file mode 100644 index 0000000..a03ebb6 --- /dev/null +++ b/resources/styles/gantt-chart.scss @@ -0,0 +1,772 @@ +.gantt-container { + width: 100%; + height: 650px; + background: rgb(var(--v-theme-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 12px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + color: rgb(var(--v-theme-on-surface)); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + overflow: hidden; +} + +.gantt-toolbar { + padding: 16px 20px; + background: rgb(var(--v-theme-surface)); + border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + display: flex; + gap: 12px; +} + +.btn { + display: inline-flex; + align-items: center; + padding: 10px 16px; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s ease; + gap: 8px; +} + +.btn-icon { + flex-shrink: 0; +} + +.btn-primary { + background: rgb(var(--v-theme-primary)); + color: rgb(var(--v-theme-on-primary)); + box-shadow: 0 2px 4px rgba(var(--v-theme-primary), 0.3); +} + +.btn-primary:hover { + background: rgba(var(--v-theme-primary), 0.8); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(var(--v-theme-primary), 0.4); +} + +.btn-danger { + background: rgb(var(--v-theme-error)); + color: rgb(var(--v-theme-on-error)); + box-shadow: 0 2px 4px rgba(var(--v-theme-error), 0.3); +} + +.btn-danger:hover { + background: rgba(var(--v-theme-error), 0.8); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(var(--v-theme-error), 0.4); +} + +.btn-success { + background: rgb(var(--v-theme-success)); + color: rgb(var(--v-theme-on-success)); + box-shadow: 0 2px 4px rgba(var(--v-theme-success), 0.3); +} + +.btn-success:hover { + background: rgba(var(--v-theme-success), 0.8); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(var(--v-theme-success), 0.4); +} + +.btn-secondary { + background: rgba(var(--v-theme-on-surface), 0.05); + color: rgb(var(--v-theme-on-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.btn-secondary:hover { + background: rgba(var(--v-theme-on-surface), 0.1); + border-color: rgba(var(--v-border-color), 0.5); + transform: translateY(-1px); +} + +.gantt-chart { + height: calc(100% - 77px); + min-height: 500px; +} + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: modalOverlayFadeIn 0.3s ease; +} + +@keyframes modalOverlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.modal-container { + background: rgb(var(--v-theme-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + width: 90%; + max-width: 520px; + max-height: 80vh; + overflow: hidden; + animation: modalSlideIn 0.3s ease; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + background: rgb(var(--v-theme-surface)); +} + +.modal-title { + display: flex; + align-items: center; + margin: 0; + font-size: 18px; + font-weight: 600; + color: rgb(var(--v-theme-on-surface)); +} + +.modal-icon { + margin-right: 10px; + flex-shrink: 0; + padding: 4px; + border-radius: 4px; +} + +.delete-icon { + color: rgb(var(--v-theme-error)); + background: rgba(var(--v-theme-error), 0.1); +} + +.edit-icon { + color: rgb(var(--v-theme-primary)); + background: rgba(var(--v-theme-primary), 0.1); +} + +.add-icon { + color: rgb(var(--v-theme-success)); + background: rgba(var(--v-theme-success), 0.1); +} + +.modal-close { + background: rgba(var(--v-theme-on-surface), 0.05); + border: none; + font-size: 18px; + color: rgb(var(--v-theme-on-surface)); + cursor: pointer; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + transition: all 0.2s ease; +} + +.modal-close:hover { + background: rgba(var(--v-theme-on-surface), 0.1); + color: rgb(var(--v-theme-error)); +} + +.modal-body { + padding: 24px; + background: rgb(var(--v-theme-surface)); +} + +.modal-message { + font-size: 15px; + color: rgb(var(--v-theme-on-surface)); + margin: 0 0 8px 0; + line-height: 1.5; +} + +.modal-warning { + font-size: 13px; + color: rgba(var(--v-theme-on-surface), 0.6); + margin: 0; + font-style: italic; +} + +.form-group { + margin-bottom: 20px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: rgb(var(--v-theme-on-surface)); + font-size: 13px; +} + +.form-input { + width: 100%; + padding: 12px 14px; + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 6px; + font-size: 14px; + color: rgb(var(--v-theme-on-surface)); + background: rgb(var(--v-theme-surface)); + transition: all 0.2s ease; + box-sizing: border-box; +} + +.form-input:focus { + outline: none; + border-color: rgb(var(--v-theme-primary)); + box-shadow: 0 0 0 2px rgba(var(--v-theme-primary), 0.2); +} + +.form-input:invalid { + border-color: rgb(var(--v-theme-error)); + box-shadow: 0 0 0 2px rgba(var(--v-theme-error), 0.2); +} + +.progress-input-container { + display: flex; + align-items: center; + gap: 12px; +} + +.form-range { + flex: 1; + height: 4px; + background: rgba(var(--v-theme-on-surface), 0.1); + border-radius: 2px; + outline: none; +} + +.form-range::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: rgb(var(--v-theme-primary)); + border-radius: 50%; + cursor: pointer; + border: 2px solid rgb(var(--v-theme-surface)); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.form-range::-moz-range-thumb { + width: 16px; + height: 16px; + background: rgb(var(--v-theme-primary)); + border-radius: 50%; + cursor: pointer; + border: 2px solid rgb(var(--v-theme-surface)); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.progress-value { + min-width: 40px; + text-align: right; + font-weight: 600; + color: rgb(var(--v-theme-primary)); + font-size: 12px; + padding: 2px 6px; + background: rgba(var(--v-theme-primary), 0.1); + border-radius: 4px; +} + +.modal-footer { + padding: 16px 24px; + display: flex; + gap: 12px; + justify-content: flex-end; + background: rgb(var(--v-theme-surface)); + border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.notification { + position: fixed; + top: 20px; + right: 20px; + min-width: 300px; + background: rgb(var(--v-theme-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + z-index: 10001; + animation: notificationSlideIn 0.3s ease; +} + +@keyframes notificationSlideIn { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.notification-content { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; +} + +.notification-message { + font-size: 14px; + color: rgb(var(--v-theme-on-surface)); + font-weight: 500; +} + +.notification-close { + background: none; + border: none; + font-size: 16px; + color: rgba(var(--v-theme-on-surface), 0.6); + cursor: pointer; + padding: 0; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + transition: all 0.2s ease; +} + +.notification-close:hover { + background: rgba(var(--v-theme-on-surface), 0.1); + color: rgb(var(--v-theme-on-surface)); +} + +.notification-success { + border-left: 3px solid rgb(var(--v-theme-success)); +} + +.notification-error { + border-left: 3px solid rgb(var(--v-theme-error)); +} + +.notification-warning { + border-left: 3px solid rgb(var(--v-theme-warning)); +} + +.notification-info { + border-left: 3px solid rgb(var(--v-theme-info)); +} + +.gantt-container:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); +} + +.gantt_container { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + background: rgb(var(--v-theme-surface)); + color: rgb(var(--v-theme-on-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 12px; + overflow: hidden; +} + +.gantt_layout_content { + background: rgb(var(--v-theme-surface)) !important; + color: rgb(var(--v-theme-on-surface)) !important; +} + +.gantt_grid_head_cell, +.gantt_grid_data .gantt_cell { + border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.gantt_grid_head_cell { + background: rgb(var(--v-theme-surface)); + font-weight: 600; + text-align: center; + padding: 12px 10px; + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.gantt_cell { + padding: 10px 12px; + vertical-align: middle; + line-height: 1.4; + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + background: inherit; +} + +.gantt_tree_content { + padding-left: 6px; + color: rgb(var(--v-theme-on-surface)); + font-weight: 500; +} + +.gantt_tree_icon { + width: 14px; + height: 14px; + margin-right: 6px; + opacity: 0.6; + color: rgb(var(--v-theme-on-surface)); +} + +.gantt_task_line.completed-task { + background: rgb(var(--v-theme-success)); + border: 1px solid rgba(var(--v-theme-success), 0.8); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(var(--v-theme-success), 0.3); +} + +.gantt_task_line.high-progress-task { + background: rgb(var(--v-theme-primary)); + border: 1px solid rgba(var(--v-theme-primary), 0.8); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(var(--v-theme-primary), 0.3); +} + +.gantt_task_line.medium-progress-task { + background: rgb(var(--v-theme-warning)); + border: 1px solid rgba(var(--v-theme-warning), 0.8); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(var(--v-theme-warning), 0.3); +} + +.gantt_task_line.low-progress-task { + background: rgb(var(--v-theme-error)); + border: 1px solid rgba(var(--v-theme-error), 0.8); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(var(--v-theme-error), 0.3); +} + +.summary-row { + background: rgba(var(--v-theme-primary), 0.1); + font-weight: 600; + color: rgb(var(--v-theme-on-surface)); + border-left: 3px solid rgb(var(--v-theme-primary)); +} + +.gantt_task_progress { + background: rgba(255, 255, 255, 0.3); + border-radius: 2px; +} + +.gantt_scale_cell { + border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + background: rgb(var(--v-theme-surface)); + font-weight: 500; + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + font-size: 11px; + text-align: center; +} + +.gantt_task_line { + border-radius: 4px; + height: 20px; + transition: all 0.2s ease; +} + +.gantt_task_line:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.gantt_side_content { + color: rgba(var(--v-theme-on-surface), 0.6); + font-size: 11px; + margin-top: 2px; + font-weight: 500; +} + +.completed-mark { + color: rgb(var(--v-theme-success)); + font-weight: bold; + font-size: 12px; +} + +.gantt_grid_data .gantt_row { + background: rgb(var(--v-theme-surface)); +} + +.gantt_grid_data .gantt_row.gantt_row_odd { + background: rgba(var(--v-theme-on-surface), 0.02); +} + +.gantt_grid_data .gantt_row:hover { + background: rgba(var(--v-theme-primary), 0.08) !important; +} + +.gantt_row:hover { + background: rgba(var(--v-theme-primary), 0.08) !important; +} + +.gantt_row_task:hover { + background: rgba(var(--v-theme-primary), 0.08) !important; +} + +.gantt_row.gantt_row_odd:hover { + background: rgba(var(--v-theme-primary), 0.1) !important; +} + +.gantt_row:hover .gantt_cell { + background: inherit !important; +} + +.gantt_row:hover .gantt_tree_content { + color: rgb(var(--v-theme-on-surface)) !important; +} + +.gantt_selected .gantt_cell { + background: rgba(var(--v-theme-primary), 0.12) !important; + color: rgb(var(--v-theme-on-surface)) !important; + border-left: 3px solid rgb(var(--v-theme-primary)) !important; +} + +.gantt_row_task.gantt_selected { + background: rgba(var(--v-theme-primary), 0.12) !important; +} + +.gantt_row.gantt_selected { + background: rgba(var(--v-theme-primary), 0.12) !important; +} + +.gantt_row.gantt_selected .gantt_tree_content { + color: rgb(var(--v-theme-on-surface)) !important; + font-weight: 600; +} + +.gantt_row.gantt_selected.gantt_row_odd { + background: rgba(var(--v-theme-primary), 0.15) !important; +} + +.gantt_selected { + background: rgba(var(--v-theme-primary), 0.12) !important; +} + +.gantt_task_link { + stroke: rgba(var(--v-theme-on-surface), 0.4); + stroke-width: 1.5; +} + +.gantt_task_link .gantt_link_arrow { + fill: rgba(var(--v-theme-on-surface), 0.4); +} + +.gantt_milestone { + background: rgb(var(--v-theme-secondary)); + border: 2px solid rgba(var(--v-theme-secondary), 0.8); + box-shadow: 0 1px 3px rgba(var(--v-theme-secondary), 0.3); +} + +.gantt-context-menu { + background: rgb(var(--v-theme-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + padding: 6px; + min-width: 180px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 13px; + z-index: 9999; + animation: contextMenuFadeIn 0.2s ease; +} + +@keyframes contextMenuFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.context-menu-item { + display: flex; + align-items: center; + padding: 8px 10px; + cursor: pointer; + color: rgb(var(--v-theme-on-surface)); + transition: background-color 0.1s ease; + border-radius: 4px; + font-size: 13px; + line-height: 1.4; + user-select: none; +} + +.context-menu-item:hover { + background: rgba(var(--v-theme-primary), 0.1); +} + +.context-menu-item:active { + background: rgba(var(--v-theme-primary), 0.15); +} + +.menu-icon { + margin-right: 8px; + opacity: 0.6; + flex-shrink: 0; + color: rgb(var(--v-theme-on-surface)); +} + +.context-menu-separator { + height: 1px; + background: rgba(var(--v-border-color), var(--v-border-opacity)); + margin: 4px 0; +} + +.gantt_layout_root { + background: rgb(var(--v-theme-surface)); +} + +.gantt_layout_root .gantt_layout_cell { + background: rgb(var(--v-theme-surface)); +} + +.gantt_grid_scale .gantt_scale_line { + background: rgb(var(--v-theme-surface)); + border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.gantt_scale_line { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); +} + +.gantt_task_content { + color: rgb(var(--v-theme-on-primary)); + font-weight: 500; + font-size: 12px; +} + +.gantt_tooltip { + background: rgb(var(--v-theme-surface)); + color: rgb(var(--v-theme-on-surface)); + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 8px 12px; + font-size: 12px; +} + +.gantt_grid { + background: rgb(var(--v-theme-surface)); +} + +.gantt_grid_scale { + background: rgb(var(--v-theme-surface)); +} + +.gantt_task_bg { + background: rgba(var(--v-theme-on-surface), 0.05); +} + +.gantt_task_bg .gantt_task_row { + background: rgb(var(--v-theme-surface)) !important; +} + +.gantt_task_bg .gantt_task_row.odd { + background: rgba(var(--v-theme-on-surface), 0.02) !important; +} + +.gantt_task_bg .gantt_task_cell { + background: inherit; + border-right: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.gantt_data_area { + background: rgb(var(--v-theme-surface)); +} + +.gantt_task_scale { + background: rgb(var(--v-theme-surface)) !important; +} + +.gantt_hor_scroll { + background: rgb(var(--v-theme-surface)); +} + +.gantt_ver_scroll { + background: rgb(var(--v-theme-surface)); +} + +.gantt_hor_scroll .gantt_scroll_hor { + background: rgba(var(--v-theme-on-surface), 0.2); +} + +.gantt_ver_scroll .gantt_scroll_ver { + background: rgba(var(--v-theme-on-surface), 0.2); +} + +.gantt_hor_scroll .gantt_scroll_hor:hover, +.gantt_ver_scroll .gantt_scroll_ver:hover { + background: rgba(var(--v-theme-on-surface), 0.4); +} + +.gantt_task_scale .gantt_scale_line { + background: rgb(var(--v-theme-surface)) !important; + color: rgb(var(--v-theme-on-surface)) !important; +} + +.gantt_task_scale .gantt_scale_cell { + background: rgb(var(--v-theme-surface)) !important; + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)) !important; + border-color: rgba(var(--v-border-color), var(--v-border-opacity)) !important; +} + +.gantt_bars_area { + background: rgb(var(--v-theme-surface)) !important; +} + +.gantt_links_area { + background: rgb(var(--v-theme-surface)) !important; +} + +.gantt_task_link .gantt_link_line_left, +.gantt_task_link .gantt_link_line_right, +.gantt_task_link .gantt_link_line_up, +.gantt_task_link .gantt_link_line_down { + background: rgba(var(--v-theme-on-surface), 0.4) !important; +} + +.gantt_task_link .gantt_link_corner { + border-color: rgba(var(--v-theme-on-surface), 0.4) !important; +} + +.gantt_task_link .gantt_link_arrow:before { + border-color: rgba(var(--v-theme-on-surface), 0.4) !important; +} \ No newline at end of file