fix: layout polish — responsive filter, scroll, resize, min cell size
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
843ac84805
commit
338ffb9c19
5 changed files with 54 additions and 10 deletions
|
|
@ -42,8 +42,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.heatmap-wrapper .heatmap-svg-area {
|
.heatmap-wrapper .heatmap-svg-area {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
min-width: 0;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heatmap-wrapper .heatmap-filter {
|
.heatmap-wrapper .heatmap-filter {
|
||||||
|
|
@ -56,6 +56,22 @@
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Small screens: filter above heatmap */
|
||||||
|
@media (max-width: 1330px) {
|
||||||
|
.heatmap-wrapper {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-wrapper .heatmap-filter {
|
||||||
|
padding-top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatmap-filter select {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.heatmap-weekend {
|
.heatmap-weekend {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +83,7 @@
|
||||||
.heatmap-stats {
|
.heatmap-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 8px 0 0;
|
padding: 12px 0 0;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: var(--tblr-secondary, #6c757d);
|
color: var(--tblr-secondary, #6c757d);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -209,8 +209,9 @@ export function renderHeatmap(
|
||||||
|
|
||||||
// Compute cell size to fill available width, capped at 16px
|
// Compute cell size to fill available width, capped at 16px
|
||||||
const containerWidth = container.clientWidth || 800;
|
const containerWidth = container.clientWidth || 800;
|
||||||
const maxCellSize = 18;
|
const maxCellSize = 22;
|
||||||
const cellSize = Math.min(maxCellSize, Math.max(2, Math.floor((containerWidth - marginLeft) / numWeeks) - cellGap));
|
const minCellSize = 10;
|
||||||
|
const cellSize = Math.min(maxCellSize, Math.max(minCellSize, Math.floor((containerWidth - marginLeft) / numWeeks) - cellGap));
|
||||||
const step = cellSize + cellGap;
|
const step = cellSize + cellGap;
|
||||||
const svgWidth = marginLeft + numWeeks * step;
|
const svgWidth = marginLeft + numWeeks * step;
|
||||||
const svgHeight = marginTop + 7 * step + marginBottom;
|
const svgHeight = marginTop + 7 * step + marginBottom;
|
||||||
|
|
@ -297,6 +298,7 @@ export function renderHeatmap(
|
||||||
if (!onCellClick) return;
|
if (!onCellClick) return;
|
||||||
onCellClick(d.dateStr);
|
onCellClick(d.dateStr);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function init(container: HTMLElement): void {
|
export function init(container: HTMLElement): void {
|
||||||
|
|
@ -331,6 +333,16 @@ export function init(container: HTMLElement): void {
|
||||||
svgArea.className = 'heatmap-svg-area';
|
svgArea.className = 'heatmap-svg-area';
|
||||||
wrapper.appendChild(svgArea);
|
wrapper.appendChild(svgArea);
|
||||||
|
|
||||||
|
// Shared state for current data (used by resize re-render and filter)
|
||||||
|
let currentData: HeatmapData | null = null;
|
||||||
|
|
||||||
|
const doRender = (data: HeatmapData, emptyMsg?: string) => {
|
||||||
|
currentData = data;
|
||||||
|
renderHeatmap(svgArea, data, DEFAULT_CONFIG, onCellClick, emptyMsg, weekStart);
|
||||||
|
renderStats(container, data.days);
|
||||||
|
svgArea.scrollLeft = svgArea.scrollWidth;
|
||||||
|
};
|
||||||
|
|
||||||
// Build filter dropdown (only if projects exist)
|
// Build filter dropdown (only if projects exist)
|
||||||
if (projects.length > 0) {
|
if (projects.length > 0) {
|
||||||
const filterDiv = document.createElement('div');
|
const filterDiv = document.createElement('div');
|
||||||
|
|
@ -363,8 +375,7 @@ export function init(container: HTMLElement): void {
|
||||||
return res.json() as Promise<HeatmapData>;
|
return res.json() as Promise<HeatmapData>;
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
renderHeatmap(svgArea, data, DEFAULT_CONFIG, onCellClick, 'No tracking data for this project', weekStart);
|
doRender(data, 'No tracking data for this project');
|
||||||
renderStats(wrapper, data.days);
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('KimaiHeatmap: failed to load filtered data', err);
|
console.error('KimaiHeatmap: failed to load filtered data', err);
|
||||||
|
|
@ -377,6 +388,15 @@ export function init(container: HTMLElement): void {
|
||||||
|
|
||||||
container.appendChild(wrapper);
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
// Re-render on window resize (debounced)
|
||||||
|
let resizeTimer: ReturnType<typeof setTimeout>;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
if (currentData) doRender(currentData);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
// Initial data fetch
|
// Initial data fetch
|
||||||
fetch(baseUrl)
|
fetch(baseUrl)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|
@ -384,8 +404,7 @@ export function init(container: HTMLElement): void {
|
||||||
return res.json() as Promise<HeatmapData>;
|
return res.json() as Promise<HeatmapData>;
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
renderHeatmap(svgArea, data, DEFAULT_CONFIG, onCellClick, undefined, weekStart);
|
doRender(data);
|
||||||
renderStats(wrapper, data.days);
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('KimaiHeatmap: failed to load data', err);
|
console.error('KimaiHeatmap: failed to load data', err);
|
||||||
|
|
|
||||||
8
assets/test/setup.ts
Normal file
8
assets/test/setup.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// jsdom doesn't provide ResizeObserver
|
||||||
|
if (typeof globalThis.ResizeObserver === 'undefined') {
|
||||||
|
globalThis.ResizeObserver = class ResizeObserver {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
} as unknown as typeof globalThis.ResizeObserver;
|
||||||
|
}
|
||||||
|
|
@ -4,5 +4,6 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
|
setupFiles: ['./assets/test/setup.ts'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue