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:
@@ -443,8 +443,9 @@ function TrackRow({
|
||||
showNowLabel?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-[52px_1fr] items-center gap-x-1 gap-y-0">
|
||||
<div className="pr-1 text-right text-[10px] font-medium text-slate-400">{label}</div>
|
||||
// Mobil: label nad trackem (plná šířka); md+: label vlevo vedle tracku.
|
||||
<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} />
|
||||
</div>
|
||||
)
|
||||
@@ -484,8 +485,8 @@ function StatePanelRaw({ slots, nowIndex }: StatePanelProps) {
|
||||
<TrackRow label="Síť" segments={gridSegs} nowIndex={nowIndex} showNowLabel />
|
||||
<TrackRow label="Baterie" segments={batSegs} nowIndex={nowIndex} />
|
||||
</div>
|
||||
<div className="mt-1 grid grid-cols-[52px_1fr] gap-x-1">
|
||||
<div />
|
||||
<div className="mt-1 grid grid-cols-1 gap-x-1 md:grid-cols-[52px_1fr]">
|
||||
<div className="hidden md:block" />
|
||||
<TickRow slots={slots} />
|
||||
</div>
|
||||
<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="TČ" segments={tcSegs} nowIndex={nowIndex} />
|
||||
</div>
|
||||
<div className="mt-1 grid grid-cols-[52px_1fr] gap-x-1">
|
||||
<div />
|
||||
<div className="mt-1 grid grid-cols-1 gap-x-1 md:grid-cols-[52px_1fr]">
|
||||
<div className="hidden md:block" />
|
||||
<TickRow slots={slots} />
|
||||
</div>
|
||||
<ul className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-slate-500">
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Upload,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Area,
|
||||
Bar,
|
||||
@@ -1054,11 +1054,6 @@ export default function Planning() {
|
||||
return new Map(list.map((i) => [i.interval_start, i]))
|
||||
}, [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)
|
||||
|
||||
async function onReplan() {
|
||||
@@ -1612,11 +1607,11 @@ export default function Planning() {
|
||||
</section>
|
||||
|
||||
{/* 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>
|
||||
<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">
|
||||
<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)]">
|
||||
<tr className="text-slate-500">
|
||||
<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 pvAllowed = pvAAllowedW(i)
|
||||
return (
|
||||
<Fragment key={i.interval_start}>
|
||||
<tr
|
||||
key={i.interval_start}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
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>
|
||||
<VynosKcCell v={i.expected_cost_czk} />
|
||||
</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>
|
||||
@@ -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ý).
|
||||
</p>
|
||||
)}
|
||||
{selectedSlot != null && (
|
||||
<PlanSlotDetail
|
||||
i={selectedSlot}
|
||||
mask={maskForInterval(solverSnap, selectedSlot.interval_start)}
|
||||
compare={compareIntervalByStart.get(selectedSlot.interval_start)}
|
||||
nowMs={nowMs}
|
||||
/>
|
||||
)}
|
||||
{!solverSnap && run != null && (
|
||||
<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.
|
||||
|
||||
Reference in New Issue
Block a user