responsivita: StatePanel label nad track na mobilu, Planning detail pod řádkem + min-w tabulky

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dusan Vojacek
2026-06-11 14:25:22 +02:00
parent ca6bd4ab2a
commit eb360da910
2 changed files with 28 additions and 23 deletions

View File

@@ -443,8 +443,9 @@ function TrackRow({
showNowLabel?: boolean showNowLabel?: boolean
}) { }) {
return ( return (
<div className="grid grid-cols-[52px_1fr] items-center gap-x-1 gap-y-0"> // Mobil: label nad trackem (plná šířka); md+: label vlevo vedle tracku.
<div className="pr-1 text-right text-[10px] font-medium text-slate-400">{label}</div> <div className="grid grid-cols-1 items-center gap-x-1 gap-y-0.5 md:grid-cols-[52px_1fr] md:gap-y-0">
<div className="pr-1 text-left text-[10px] font-medium text-slate-400 md:text-right">{label}</div>
<SegmentBar segments={segments} nowIndex={nowIndex} showNowLabel={showNowLabel} /> <SegmentBar segments={segments} nowIndex={nowIndex} showNowLabel={showNowLabel} />
</div> </div>
) )
@@ -484,8 +485,8 @@ function StatePanelRaw({ slots, nowIndex }: StatePanelProps) {
<TrackRow label="Síť" segments={gridSegs} nowIndex={nowIndex} showNowLabel /> <TrackRow label="Síť" segments={gridSegs} nowIndex={nowIndex} showNowLabel />
<TrackRow label="Baterie" segments={batSegs} nowIndex={nowIndex} /> <TrackRow label="Baterie" segments={batSegs} nowIndex={nowIndex} />
</div> </div>
<div className="mt-1 grid grid-cols-[52px_1fr] gap-x-1"> <div className="mt-1 grid grid-cols-1 gap-x-1 md:grid-cols-[52px_1fr]">
<div /> <div className="hidden md:block" />
<TickRow slots={slots} /> <TickRow slots={slots} />
</div> </div>
<ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-slate-500"> <ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-slate-500">
@@ -517,8 +518,8 @@ function StatePanelRaw({ slots, nowIndex }: StatePanelProps) {
<TrackRow label="Zoe" segments={ev2Segs} nowIndex={nowIndex} /> <TrackRow label="Zoe" segments={ev2Segs} nowIndex={nowIndex} />
<TrackRow label="TČ" segments={tcSegs} nowIndex={nowIndex} /> <TrackRow label="TČ" segments={tcSegs} nowIndex={nowIndex} />
</div> </div>
<div className="mt-1 grid grid-cols-[52px_1fr] gap-x-1"> <div className="mt-1 grid grid-cols-1 gap-x-1 md:grid-cols-[52px_1fr]">
<div /> <div className="hidden md:block" />
<TickRow slots={slots} /> <TickRow slots={slots} />
</div> </div>
<ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-slate-500"> <ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-slate-500">

View File

@@ -9,7 +9,7 @@ import {
Upload, Upload,
} from 'lucide-react' } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useCallback, useEffect, useMemo, useState } from 'react' import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
import { import {
Area, Area,
Bar, Bar,
@@ -1054,11 +1054,6 @@ export default function Planning() {
return new Map(list.map((i) => [i.interval_start, i])) return new Map(list.map((i) => [i.interval_start, i]))
}, [compareData?.comparison?.intervals]) }, [compareData?.comparison?.intervals])
const selectedSlot = useMemo(
() => visibleSlots.find((s) => s.interval_start === selectedStart) ?? null,
[visibleSlots, selectedStart],
)
const tableColCount = 14 + (solverSnap != null ? 1 : 0) + (showGenCut ? 1 : 0) const tableColCount = 14 + (solverSnap != null ? 1 : 0) + (showGenCut ? 1 : 0)
async function onReplan() { async function onReplan() {
@@ -1612,11 +1607,11 @@ export default function Planning() {
</section> </section>
{/* Sekce 4 */} {/* Sekce 4 */}
<section className="rounded-xl border border-slate-800 bg-slate-900/40 p-4 shadow-lg"> <section className="relative rounded-xl border border-slate-800 bg-slate-900/40 p-4 shadow-lg">
<h2 className="mb-1 text-sm font-medium uppercase tracking-wide text-slate-400">Tabulka slotů</h2> <h2 className="mb-1 text-sm font-medium uppercase tracking-wide text-slate-400">Tabulka slotů</h2>
<HorizonToggle value={tableHorizonH} onChange={setTableHorizonH} disabled={futureSlots.length === 0} /> <HorizonToggle value={tableHorizonH} onChange={setTableHorizonH} disabled={futureSlots.length === 0} />
<div className="max-h-[min(70vh,720px)] overflow-y-auto overflow-x-auto rounded-lg border border-slate-800/80"> <div className="max-h-[min(70vh,720px)] overflow-y-auto overflow-x-auto rounded-lg border border-slate-800/80">
<table className="w-full border-collapse text-left text-xs"> <table className="w-full min-w-[1100px] border-collapse text-left text-xs">
<thead className="sticky top-0 z-10 bg-slate-900 shadow-[0_1px_0_0_rgb(30_41_59)]"> <thead className="sticky top-0 z-10 bg-slate-900 shadow-[0_1px_0_0_rgb(30_41_59)]">
<tr className="text-slate-500"> <tr className="text-slate-500">
<th className="whitespace-nowrap py-2 pl-2 pr-2 font-medium">Čas</th> <th className="whitespace-nowrap py-2 pl-2 pr-2 font-medium">Čas</th>
@@ -1716,8 +1711,8 @@ export default function Planning() {
const phaseBadge = negSellPhaseBadge(slotMask) const phaseBadge = negSellPhaseBadge(slotMask)
const pvAllowed = pvAAllowedW(i) const pvAllowed = pvAAllowedW(i)
return ( return (
<Fragment key={i.interval_start}>
<tr <tr
key={i.interval_start}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setSelectedStart((prev) => (prev === i.interval_start ? null : i.interval_start))} onClick={() => setSelectedStart((prev) => (prev === i.interval_start ? null : i.interval_start))}
@@ -1816,6 +1811,23 @@ export default function Planning() {
<td className="pr-2 text-slate-300">{i.heat_pump_enabled ? 'on' : 'off'}</td> <td className="pr-2 text-slate-300">{i.heat_pump_enabled ? 'on' : 'off'}</td>
<VynosKcCell v={i.expected_cost_czk} /> <VynosKcCell v={i.expected_cost_czk} />
</tr> </tr>
{sel ? (
// Detail jako plnoširoký řádek pod vybraným slotem (žádný překryv);
// sticky left drží blok ve viewportu i při horizontálním scrollu tabulky.
<tr className="border-b border-slate-800/80">
<td colSpan={tableColCount} className="p-0">
<div className="sticky left-0 w-[min(100%,calc(100vw-4rem))] px-2 pb-3">
<PlanSlotDetail
i={i}
mask={slotMask}
compare={compareIntervalByStart.get(i.interval_start)}
nowMs={nowMs}
/>
</div>
</td>
</tr>
) : null}
</Fragment>
) )
})} })}
</tbody> </tbody>
@@ -1826,14 +1838,6 @@ export default function Planning() {
Žádné budoucí sloty v horizontu {tableHorizonH} h (aktivní plán může být prázdný nebo starý). Žádné budoucí sloty v horizontu {tableHorizonH} h (aktivní plán může být prázdný nebo starý).
</p> </p>
)} )}
{selectedSlot != null && (
<PlanSlotDetail
i={selectedSlot}
mask={maskForInterval(solverSnap, selectedSlot.interval_start)}
compare={compareIntervalByStart.get(selectedSlot.interval_start)}
nowMs={nowMs}
/>
)}
{!solverSnap && run != null && ( {!solverSnap && run != null && (
<p className="mt-2 text-[11px] text-slate-500"> <p className="mt-2 text-[11px] text-slate-500">
Masky solveru nejsou v tomto běhu spusťte nový rolling/denní plán po nasazení arbitráže. Masky solveru nejsou v tomto běhu spusťte nový rolling/denní plán po nasazení arbitráže.