diff --git a/frontend/src/app.css b/frontend/src/app.css index af03b56..73c1f3d 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -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; } diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 4d5cd2f..b1c0269 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -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 (
Kennzahlen
+ {touchUi && ( +
+ + Auf dem Smartphone: für Erklärung und Details. +
+ )}
{tiles.map(t => { const accent = getStatusColor(t.status) @@ -246,10 +285,21 @@ function BodyKpiOverview({ tiles }) {
-
+ {touchUi && ( + + )} +
{t.icon}
{t.category}
@@ -266,6 +316,34 @@ function BodyKpiOverview({ tiles }) { ) })}
+ + {openParts && ( +
setOpenKey(null)} + > +
e.stopPropagation()} + > +
+

{openParts.title}

+ +
+ {openParts.body ? ( +
{openParts.body}
+ ) : ( +
Keine weiteren Details.
+ )} +
+
+ )}
) }