layout: 320 px ohne Ueberstand, Schubladen in voller Breite, Doppelklick-Sperre greift sofort

Befunde aus der Sichtpruefung (Attrappe des Backends, 320/768/1024/1440 px):
- Zaehlwerk: Das "H" ragte bei 320 px 30 px ueber sein Feld. Ziffern skalieren jetzt
  mit der Feldbreite (Container-Einheiten), am PC bleibt es bei 38 x 56 px.
- Flugplan und Update-Verlauf: Der Text bekam schmal nur ~50 px und lief in den Zustand.
  Schmales Feld: Zeit und Zustand oben, Text darunter (nach Feldbreite, nicht Bildschirm).
- Update-Zeilen am Handy: Name und Version stiessen aneinander ("BetriebssystemUbuntu"),
  die Version steht jetzt darunter; scheitert die Pruefung, heisst sie "unbekannt" statt "-".
- Schubladen (Modell-Suche, Dienste, Einstellungen, Protokoll) waren am Handy nur 75 %
  und am PC hoechstens 384 px breit - die shadcn-Grundklassen schlugen die eigenen.
- Doppelklick: Die Sperre beim Installieren haengt jetzt an einem Ref und greift sofort
  (vorher starteten zwei Klicks im selben Takt zwei Downloads); die Ja-Knoepfe von
  "Alles aktualisieren" und "Auftrag abbrechen" ignorieren den zweiten Klick.
- Balken eines abgebrochenen Downloads grau statt cyan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 15:36:03 +02:00
co-authored by Claude Opus 5.5
parent 634ad27302
commit a091ccb3b8
8 changed files with 58 additions and 32 deletions
+8 -5
View File
@@ -8,18 +8,21 @@ const STATUS: Record<FlugplanEintrag["status"], { text: string; farbe: string }>
geplant: { text: "Geplant", farbe: "text-text-3" },
}
// Die Liste richtet sich nach der Breite ihres Feldes, nicht des Bildschirms (@container): Schmal —
// am Handy und in der rechten Spalte bei 1024 px — stehen Zeit und Zustand oben, der Text darunter
// über die volle Breite. Vorher bekam der Text dort ~50 px und lief in den Zustand hinein.
function Zeile({ e }: { e: FlugplanEintrag }) {
const s = STATUS[e.status] ?? STATUS.geplant
// Updates sind das Wichtigste im Plan — sie stehen in Cyan.
const farbe = e.status === "geplant" && /update/i.test(e.titel) ? "text-cyan" : s.farbe
return (
<li className="grid grid-cols-[112px_minmax(0,1fr)_auto] items-center gap-3 border-t border-linie py-2.5">
<span className="ziffern text-sm text-text-3">{flugplanZeit(e.zeit)}</span>
<span className="min-w-0 text-base">
<li className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 border-t border-linie py-2.5 @sm:grid-cols-[112px_minmax(0,1fr)_auto]">
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{flugplanZeit(e.zeit)}</span>
<span className="col-span-2 row-start-2 min-w-0 text-base break-words @sm:col-span-1 @sm:col-start-2 @sm:row-start-1">
{e.titel}
{e.text && <span className="text-text-3">, {e.text}</span>}
</span>
<span className={cn("schild text-[13px]", farbe)}>{s.text}</span>
<span className={cn("schild col-start-2 row-start-1 justify-self-end text-[13px] @sm:col-start-3", farbe)}>{s.text}</span>
</li>
)
}
@@ -27,5 +30,5 @@ function Zeile({ e }: { e: FlugplanEintrag }) {
export function Flugplan({ gelaufen, geplant }: { gelaufen: FlugplanEintrag[]; geplant: FlugplanEintrag[] }) {
const alle = [...gelaufen, ...geplant.slice(0, 5)]
if (alle.length === 0) return <p className="text-[15px] text-text-2">Heute ist noch nichts gelaufen, und nichts ist geplant.</p>
return <ol className="m-0 flex list-none flex-col p-0">{alle.map((e) => <Zeile key={`${e.titel}-${e.zeit}`} e={e} />)}</ol>
return <ol className="@container m-0 flex list-none flex-col p-0">{alle.map((e) => <Zeile key={`${e.titel}-${e.zeit}`} e={e} />)}</ol>
}
+7 -1
View File
@@ -1,4 +1,5 @@
import { useState } from "react"
import { cn } from "cn"
import { Button } from "@/components/ui/button"
import { laeuftNoch } from "@/lib/anzeige"
import { abbrechbar, fortschrittText, jobDownload, jobTitel, laufText, letzteMeldung } from "@/lib/wartung"
@@ -15,6 +16,10 @@ const ZUSTAND: Record<JobZustand, { text: string; art: ChipArt }> = {
canceled: { text: "Abgebrochen", art: "grau" },
}
const BALKEN: Record<JobZustand, string> = {
queued: "bg-cyan", running: "bg-cyan", done: "bg-gruen", failed: "bg-rot", canceled: "bg-text-3",
}
const zeitVon = (sekunden: number) => flugplanZeit(new Date(sekunden * 1000).toISOString())
/**
@@ -53,7 +58,7 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt }: {
className="h-2 overflow-hidden rounded-full bg-linie"
>
<div
className={job.state === "failed" ? "h-full bg-rot" : "h-full bg-cyan"}
className={cn("h-full", BALKEN[job.state] ?? "bg-cyan")}
style={{ width: `${Math.max(2, Math.min(100, job.progress))}%` }}
/>
</div>
@@ -105,6 +110,7 @@ export function JobKarte({ job, onAbbrechen, abbrechenGesperrt }: {
neinKnopf="Weiterlaufen lassen"
onNein={() => setFrage(false)}
onJa={() => {
if (!frage) return // zweiter Klick eines Doppelklicks
setFrage(false)
onAbbrechen?.(job)
}}
@@ -11,7 +11,7 @@ export function Protokollfenster({ offen, titel, text, onSchliessen }: {
}) {
return (
<Sheet open={offen} onOpenChange={(o) => !o && onSchliessen()}>
<SheetContent side="right" className="w-full border-linie bg-panel sm:max-w-2xl">
<SheetContent side="right" className="border-linie bg-panel data-[side=right]:w-full data-[side=right]:sm:max-w-2xl">
<SheetHeader>
<SheetTitle className="schild text-lg">Protokoll</SheetTitle>
<SheetDescription className="text-text-2">{titel}</SheetDescription>
+13 -5
View File
@@ -1,8 +1,11 @@
import { laufzeit } from "@/lib/zeit"
// Alle Maße hängen an der Schriftgröße der Zeile (em), und die wächst mit der Breite des Feldes
// (cqw): So passt „TT T SS H“ auch in die schmale Hälfte eines 320-px-Handys, und am PC bleibt
// es bei den alten 38 × 56 px je Ziffer.
function Ziffer({ z }: { z: string }) {
return (
<span className="ziffern inline-flex h-14 w-[38px] items-center justify-center rounded-md border border-[#2a3036] bg-[#0b0d0f] text-[32px] font-medium">
<span className="ziffern inline-flex h-[1.75em] w-[1.1875em] shrink-0 items-center justify-center rounded-md border border-[#2a3036] bg-[#0b0d0f] font-medium">
{z}
</span>
)
@@ -15,14 +18,19 @@ export function Zaehlwerk({ sekunden }: { sekunden: number | null | undefined })
const std = String(lz?.[1] ?? 0).padStart(2, "0")
const text = lz ? `Laufzeit ohne Neustart: ${lz[0]} Tage, ${lz[1]} Stunden` : "Laufzeit unbekannt"
return (
<figure className="m-0 flex min-w-0 flex-col items-center justify-end gap-1">
<div aria-label={text} role="img" className="flex h-[150px] max-w-full items-center gap-1.5 pb-5">
<figure className="@container m-0 flex min-w-0 flex-col items-center justify-end gap-1">
{/* Höhe wie die Rundinstrumente daneben (200 × 150, also ¾ der Breite). */}
<div
aria-label={text}
role="img"
className="flex h-[min(75cqw,150px)] items-center gap-[0.1875em] pb-[min(10cqw,20px)] text-[length:clamp(14px,14.5cqw,32px)]"
>
<Ziffer z={lz ? tage[0] : "–"} />
<Ziffer z={lz ? tage[1] : "–"} />
<span className="schild mr-2 ml-0.5 text-lg font-bold text-text-3">T</span>
<span className="schild mr-[0.444em] ml-[0.111em] text-[0.5625em] font-bold text-text-3">T</span>
<Ziffer z={lz ? std[0] : "–"} />
<Ziffer z={lz ? std[1] : "–"} />
<span className="schild ml-0.5 text-lg font-bold text-text-3">H</span>
<span className="schild ml-[0.111em] text-[0.5625em] font-bold text-text-3">H</span>
</div>
<figcaption className="schild text-[15px] text-text-3">Ohne Neustart</figcaption>
</figure>