feat: implement touch-friendly KPI details with bottom sheet interaction
- Added a new button for displaying KPI details on touch devices, replacing hover functionality. - Introduced a bottom sheet component to present detailed information when the info button is clicked. - Enhanced the BodyKpiOverview component to detect touch UI and adjust interactions accordingly. - Updated CSS styles for new touch elements, ensuring a responsive and user-friendly design.
This commit is contained in:
parent
8fc7d9c1c4
commit
8c60601ed1
|
|
@ -220,6 +220,113 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
|
|||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
/* Kennzahlen: Touch — iOS hat kein Hover; ℹ öffnet Bottom-Sheet */
|
||||
.body-kpi-info-btn {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text3);
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.body-kpi-info-btn:active {
|
||||
background: var(--surface);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.body-kpi-touch-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10050;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
animation: body-kpi-fade-in 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes body-kpi-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
max-height: min(72vh, 560px);
|
||||
overflow: auto;
|
||||
margin: 0 auto;
|
||||
padding: 14px 16px 18px;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text1);
|
||||
line-height: 1.3;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet__close {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: -6px -8px 0 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--text2);
|
||||
font-size: 26px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet__close:active {
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet__body {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text2);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.body-kpi-touch-sheet__body--muted {
|
||||
color: var(--text3);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.history-page__title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
|
||||
ReferenceLine, PieChart, Pie, Cell, ComposedChart
|
||||
} from 'recharts'
|
||||
import { ChevronRight, Brain, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
|
||||
import { ChevronRight, Brain, ChevronDown, ChevronUp, Trash2, Info } from 'lucide-react'
|
||||
import { api } from '../utils/api'
|
||||
import { photoMonthKey, photoSortKey, formatPhotoCaption } from '../utils/photoDisplay'
|
||||
import { getBfCategory } from '../utils/calc'
|
||||
|
|
@ -232,12 +232,51 @@ function buildBodyKpiTiles({
|
|||
return tiles
|
||||
}
|
||||
|
||||
/** KPI-Kacheln: Kurzvergleich sichtbar, ausführlicher Text per nativem Hover (`title`). */
|
||||
function kpiTileDetailParts(t) {
|
||||
const registryLine = t.keys?.length ? `Registry: ${t.keys.join(', ')}` : ''
|
||||
const body = [t.hoverBody, registryLine].filter(Boolean).join('\n\n')
|
||||
return { title: t.hoverTop || t.category, body }
|
||||
}
|
||||
|
||||
/** KPI-Kacheln: Desktop — Hover (`title`). Touch — ℹ öffnet gleichen Text im Bottom-Sheet (iOS hat kein Hover). */
|
||||
function BodyKpiOverview({ tiles }) {
|
||||
const [touchUi, setTouchUi] = useState(false)
|
||||
const [openKey, setOpenKey] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(hover: none)')
|
||||
const apply = () => setTouchUi(mq.matches)
|
||||
apply()
|
||||
mq.addEventListener('change', apply)
|
||||
return () => mq.removeEventListener('change', apply)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!openKey) return
|
||||
const onKey = e => { if (e.key === 'Escape') setOpenKey(null) }
|
||||
const prev = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => {
|
||||
document.body.style.overflow = prev
|
||||
window.removeEventListener('keydown', onKey)
|
||||
}
|
||||
}, [openKey])
|
||||
|
||||
if (!tiles?.length) return null
|
||||
|
||||
const openTile = openKey ? tiles.find(x => x.key === openKey) : null
|
||||
const openParts = openTile ? kpiTileDetailParts(openTile) : null
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8 }}>Kennzahlen</div>
|
||||
{touchUi && (
|
||||
<div style={{ fontSize: 10, color: 'var(--text3)', marginBottom: 8, lineHeight: 1.4 }}>
|
||||
<Info size={12} style={{ display: 'inline', verticalAlign: 'middle', marginRight: 4 }} aria-hidden />
|
||||
Auf dem Smartphone: <strong>ℹ</strong> für Erklärung und Details.
|
||||
</div>
|
||||
)}
|
||||
<div className="body-kpi-overview">
|
||||
{tiles.map(t => {
|
||||
const accent = getStatusColor(t.status)
|
||||
|
|
@ -246,10 +285,21 @@ function BodyKpiOverview({ tiles }) {
|
|||
<div
|
||||
key={t.key}
|
||||
className="body-kpi-card"
|
||||
style={{ borderLeft: `4px solid ${accent}` }}
|
||||
title={tip}
|
||||
style={{ borderLeft: `4px solid ${accent}`, position: 'relative' }}
|
||||
title={touchUi ? undefined : tip}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6 }}>
|
||||
{touchUi && (
|
||||
<button
|
||||
type="button"
|
||||
className="body-kpi-info-btn"
|
||||
aria-label={`Details: ${t.category}`}
|
||||
aria-expanded={openKey === t.key}
|
||||
onClick={() => setOpenKey(k => (k === t.key ? null : t.key))}
|
||||
>
|
||||
<Info size={16} strokeWidth={2.25} aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6, paddingRight: touchUi ? 28 : 0 }}>
|
||||
<span style={{ fontSize: 14, lineHeight: 1 }}>{t.icon}</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.04em' }}>{t.category}</div>
|
||||
|
|
@ -266,6 +316,34 @@ function BodyKpiOverview({ tiles }) {
|
|||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{openParts && (
|
||||
<div
|
||||
className="body-kpi-touch-backdrop"
|
||||
role="presentation"
|
||||
onClick={() => setOpenKey(null)}
|
||||
>
|
||||
<div
|
||||
className="body-kpi-touch-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="body-kpi-touch-title"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="body-kpi-touch-sheet__head">
|
||||
<h3 id="body-kpi-touch-title" className="body-kpi-touch-sheet__title">{openParts.title}</h3>
|
||||
<button type="button" className="body-kpi-touch-sheet__close" onClick={() => setOpenKey(null)} aria-label="Schließen">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{openParts.body ? (
|
||||
<div className="body-kpi-touch-sheet__body">{openParts.body}</div>
|
||||
) : (
|
||||
<div className="body-kpi-touch-sheet__body body-kpi-touch-sheet__body--muted">Keine weiteren Details.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user