diff --git a/src/static/css/style.css b/src/static/css/style.css index d944697..d38956a 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -2453,25 +2453,25 @@ a.dev-source-pill:hover { /* Achsen-Container */ .ht-axis { position: relative; - height: 110px; + height: 150px; } /* Stündliches Layout: höher wegen Datums-Markern oben */ .ht-axis--hourly { - height: 130px; + height: 170px; } -/* Punkte-Bereich (über der Linie) */ -.ht-points { +/* Saeulen-Bereich (ueber der Linie) */ +.ht-bars { position: absolute; left: 4%; right: 4%; - top: 0; - height: 56px; + top: 22px; + bottom: 60px; } -.ht-axis--hourly .ht-points { - top: 20px; +.ht-axis--hourly .ht-bars { + top: 42px; } /* Achsenlinie */ @@ -2479,13 +2479,14 @@ a.dev-source-pill:hover { position: absolute; left: 2%; right: 2%; - top: 60px; + top: 100px; height: 2px; background: var(--border); + z-index: 3; } .ht-axis--hourly .ht-axis-line { - top: 80px; + top: 120px; } /* Datums-Marker (vertikale Linie + Datum oben, nur bei Stunden-Granularität) */ @@ -2639,12 +2640,12 @@ a.dev-source-pill:hover { position: absolute; left: 4%; right: 4%; - top: 72px; + top: 110px; height: 20px; } .ht-axis--hourly .ht-axis-labels { - top: 90px; + top: 130px; } .ht-axis-label { @@ -2664,6 +2665,206 @@ a.dev-source-pill:hover { color: var(--text-tertiary); } +/* === Säulen-Variante (neuer Stil) === */ +.ht-bar { + position: absolute; + bottom: 0; + width: 12px; + margin-left: -6px; + height: 100%; + cursor: pointer; + z-index: 5; + transition: transform 0.15s ease; +} +.ht-bar:hover { transform: scaleX(1.15); } +.ht-bar-fill { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--text-disabled); + border-radius: 2px 2px 0 0; + transition: background 0.2s ease, height 0.2s ease; +} +.ht-bar:hover .ht-bar-fill, +.ht-bar.active .ht-bar-fill { background: var(--accent); } +.ht-bar-fill--empty { + height: 4px; + background: var(--border); +} +.ht-bar-snap-cap { + position: absolute; + top: -8px; + left: -2px; + right: -2px; + height: 6px; + background: var(--accent); + border-radius: 2px; + box-shadow: var(--glow-accent); +} +.ht-bars:has(.ht-bar.active) .ht-bar:not(.active) { opacity: 0.45; } + +/* Aktiver Bar: dezenter Marker oben */ +.ht-bar.active::before { + content: ''; + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid var(--accent); + margin-bottom: 4px; +} + +/* Themen-Label ueber aktiven Buckets */ +.ht-bucket-label { + position: absolute; + bottom: calc(100% + 14px); + left: 50%; + transform: translateX(-50%); + font-size: 10px; + font-family: var(--font-mono); + font-weight: 600; + color: var(--accent); + background: var(--bg-card); + padding: 1px 6px; + border-radius: 8px; + border: 1px solid var(--tint-accent-strong); + white-space: nowrap; + pointer-events: none; + opacity: 0.85; +} + +/* Hover-Vorschaukarte */ +.ht-hover-card { + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + min-width: 240px; + max-width: 320px; + background: var(--bg-card); + border: 1px solid var(--accent); + border-radius: var(--radius); + box-shadow: var(--shadow-md); + padding: 8px 10px; + opacity: 0; + visibility: hidden; + transition: opacity 0.15s ease, visibility 0.15s ease; + pointer-events: none; + z-index: 20; + text-align: left; + white-space: normal; +} +.ht-bar:hover .ht-hover-card { opacity: 1; visibility: visible; } +.ht-hover-card-head { + font-size: 11px; + font-family: var(--font-mono); + color: var(--accent); + font-weight: 600; + margin-bottom: 4px; +} +.ht-hover-card-list { + list-style: none; + margin: 0; + padding: 0; + font-size: 12px; + color: var(--text-primary); + line-height: 1.4; +} +.ht-hover-card-list li { + padding: 2px 0; + border-top: 1px dashed var(--border); +} +.ht-hover-card-list li:first-child { border-top: none; } +.ht-hover-card-more { + margin-top: 4px; + font-size: 11px; + color: var(--text-tertiary); + font-style: italic; +} +.ht-hover-card-snap { + margin-top: 4px; + font-size: 11px; + color: var(--accent); + font-weight: 500; +} + +/* Lagebericht-Linien quer durch die Achse */ +.ht-snapshot-lines { + position: absolute; + left: 4%; + right: 4%; + top: 0; + bottom: 38px; + pointer-events: none; + z-index: 2; +} +.ht-snapshot-line { + position: absolute; + top: 0; + bottom: 0; + width: 1px; + margin-left: -1px; + background: var(--accent); + opacity: 0.35; + cursor: pointer; + pointer-events: auto; + transition: opacity 0.15s ease; +} +.ht-snapshot-line:hover { opacity: 0.85; } +.ht-snapshot-line-cap { + position: absolute; + top: -2px; + left: 50%; + transform: translateX(-50%); + width: 14px; + height: 14px; + background: var(--bg-card); + border: 1px solid var(--accent); + border-radius: 50%; + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; +} + +/* "Heute"-Linie */ +.ht-today-line { + position: absolute; + left: 4%; + top: 0; + bottom: 38px; + width: 1px; + margin-left: -1px; + background: var(--accent); + opacity: 0.5; + border-left: 1px dashed var(--accent); + pointer-events: none; + z-index: 4; +} +.ht-today-label { + position: absolute; + top: 0; + left: 4px; + font-size: 9px; + font-family: var(--font-mono); + font-weight: 700; + color: var(--accent); + background: var(--bg-card); + padding: 0 4px; + border-radius: 2px; +} + +@media (prefers-reduced-motion: reduce) { + .ht-bar { transition: none; } + .ht-bar-fill { transition: none; } + .ht-detail-panel { animation: none; } +} + /* Detail-Panel */ .ht-detail-panel { margin-top: 8px; diff --git a/src/static/js/app.js b/src/static/js/app.js index 79f6a02..bae85f8 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -1135,27 +1135,26 @@ const App = { entries.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0)); const granularity = this._calcGranularity(entries, range); - let buckets = this._buildBuckets(entries, granularity); - buckets = this._mergeCloseBuckets(buckets); + // Bei Filter "Lageberichte" zaehlen Snapshots; bei "Meldungen" nur Artikel; bei "Alle" beides. + const buckets = this._buildBuckets(entries, granularity); // Aktiven Index validieren if (this._activePointIndex !== null && this._activePointIndex >= buckets.length) { this._activePointIndex = null; } - // Achsen-Bereich + // Achsen-Bereich (mit etwas Padding rechts/links, damit Bars nicht abgeschnitten werden) const rangeStart = buckets[0].timestamp; const rangeEnd = buckets[buckets.length - 1].timestamp; - const maxCount = Math.max(...buckets.map(b => b.entries.length)); + const articleMax = Math.max(1, ...buckets.map(b => b.entries.filter(e => e.kind === 'article').length)); - // Stunden- vs. Tages-Granularität const isHourly = granularity === 'hour'; - const axisLabels = this._buildAxisLabels(buckets, granularity, true); + const axisLabels = this._buildAxisLabels(buckets, granularity, isHourly); + const topActiveIdx = new Set(this._pickTopActiveBucketIdx(buckets, 3)); - // HTML aufbauen - let html = `