fix(index): improve mobile dashboard layout

- Move System History action from the 3X-UI card into the System Load
  card's #extra slot so the chart opener sits next to live load values.
- Fix card widths on mobile by switching :sm="24" to :xs="24"; the sm
  breakpoint only kicks in at >=576px, so phones in portrait had no
  span set and cards shrank to content width.
- Restore vertical spacing between cards (vertical gutter was 0 on
  mobile) and reduce content padding on small screens, reserving 64px
  top so the sidebar drawer handle no longer overlaps the StatusCard.
- Wrap the 3X-UI link tags in a flex container so version/Telegram/docs
  chips wrap with consistent spacing on narrow widths.
- Make Sparkline's viewBox track its actual rendered pixel width via
  ResizeObserver so X-axis time labels stop being squashed horizontally
  by preserveAspectRatio="none" on narrow containers.
- Make the SystemHistory modal width responsive (95vw on mobile, was a
  fixed 900px that overflowed phone viewports).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-09 19:03:09 +02:00
parent 439f4cf1e8
commit b885a1f8a6
4 changed files with 90 additions and 31 deletions
+34 -4
View File
@@ -1,5 +1,5 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
const props = defineProps({
data: { type: Array, required: true },
@@ -36,8 +36,37 @@ const props = defineProps({
const hoverIdx = ref(-1);
const viewBoxAttr = computed(() => `0 0 ${props.vbWidth} ${props.height}`);
const drawWidth = computed(() => Math.max(1, props.vbWidth - props.paddingLeft - props.paddingRight));
// Measured CSS width of the SVG. Drives the viewBox so SVG units stay
// 1:1 with rendered pixels — otherwise `preserveAspectRatio="none"`
// stretches the X axis and squashes axis text horizontally on narrow
// containers (mobile). Falls back to the prop until the first measure.
const svgRef = ref(null);
const measuredWidth = ref(0);
const effectiveVbWidth = computed(() => measuredWidth.value > 0 ? measuredWidth.value : props.vbWidth);
let resizeObserver = null;
function measure() {
const el = svgRef.value;
if (!el) return;
const w = el.getBoundingClientRect?.().width || 0;
if (w > 0) measuredWidth.value = Math.round(w);
}
onMounted(() => {
measure();
if (typeof ResizeObserver !== 'undefined' && svgRef.value) {
resizeObserver = new ResizeObserver(measure);
resizeObserver.observe(svgRef.value);
} else {
window.addEventListener('resize', measure);
}
});
onBeforeUnmount(() => {
if (resizeObserver) resizeObserver.disconnect();
else window.removeEventListener('resize', measure);
});
const viewBoxAttr = computed(() => `0 0 ${effectiveVbWidth.value} ${props.height}`);
const drawWidth = computed(() => Math.max(1, effectiveVbWidth.value - props.paddingLeft - props.paddingRight));
const drawHeight = computed(() => Math.max(1, props.height - props.paddingTop - props.paddingBottom));
const nPoints = computed(() => Math.min(props.data.length, props.maxPoints));
@@ -164,7 +193,7 @@ function onMouseMove(evt) {
if (!props.showTooltip || pointsArr.value.length === 0) return;
const rect = evt.currentTarget.getBoundingClientRect();
const px = evt.clientX - rect.left;
const x = (px / rect.width) * props.vbWidth;
const x = (px / rect.width) * effectiveVbWidth.value;
const n = nPoints.value;
const dx = n > 1 ? drawWidth.value / (n - 1) : 0;
const idx = Math.max(0, Math.min(n - 1, Math.round((x - props.paddingLeft) / (dx || 1))));
@@ -192,6 +221,7 @@ const gradId = `spkGrad-${Math.random().toString(36).slice(2, 9)}`;
<template>
<svg
ref="svgRef"
width="100%"
:height="height"
:viewBox="viewBoxAttr"