Rezepte: Listenbreite ziehbar, lange Namen brechen um
Die Rezeptliste hing auf hoechstens 320px fest, und .btn ist nowrap - ein Name wie "Cheesecake mit Streuseln und Fruchteinlage" lief damit einfach aus der Spalte heraus und war abgeschnitten. Jetzt sitzt zwischen Liste und Detail ein Griff zum Ziehen, Bedienung wie der Spaltengriff der Tabellen (Zeiger-Ereignisse, col-resize). Die Breite wird gemerkt und zwischen 180 und 640 Pixel gehalten: darunter ist die Liste unlesbar, darueber bleibt fuer die Zutaten nichts mehr uebrig. Dazu eine kleine Klasse .btn.umbruch, die das nowrap aufhebt - ein Knopf, dessen Beschriftung umbrechen darf, ist auch anderswo brauchbar. In der laufenden Oberflaeche nachgemessen: Ziehen um 120px ergibt genau 120px mehr und landet im Speicher, die Grenzen klemmen bei 180 und 640, und der lange Name braucht mit der Klasse zwei Zeilen (Text 372px bei 372px Spalte) statt ueber den Rand zu laufen (434px). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8de222072c
commit
9f5d0a2310
@@ -22,6 +22,12 @@ import { fmt } from "../units";
|
||||
const BASIS_KURZ = { gram: "g", milliliter: "ml", piece: "Stk" };
|
||||
const kurz = (basis) => BASIS_KURZ[basis] || basis || "";
|
||||
|
||||
// Breite der Rezeptliste: gemerkt, mit Grenzen. Unter 180 ist die Liste
|
||||
// unlesbar, ueber 640 bleibt fuer die Zutaten nichts mehr uebrig.
|
||||
const REZEPT_BREITE = "rezepte:listenbreite";
|
||||
const BREITE_MIN = 180;
|
||||
const BREITE_MAX = 640;
|
||||
|
||||
const ZUSTAND = {
|
||||
vorhanden: { text: "vorhanden", klasse: "ok", icon: "check" },
|
||||
zu_wenig: { text: "zu wenig", klasse: "warn", icon: "alert" },
|
||||
@@ -54,6 +60,33 @@ export default function Rezepte() {
|
||||
const [rezept, setRezept] = useState(null);
|
||||
const [portionen, setPortionen] = useState(urlPortionen);
|
||||
const [fehler, setFehler] = useState(null);
|
||||
|
||||
// Breite der Rezeptliste. Gemerkt wie die Spaltenbreiten der Tabellen: Wer
|
||||
// lange Rezeptnamen hat, will mehr Platz - und zwar dauerhaft.
|
||||
const [listenBreite, setListenBreite] = useState(() => {
|
||||
const roh = Number(localStorage.getItem(REZEPT_BREITE));
|
||||
return Number.isFinite(roh) && roh >= BREITE_MIN ? Math.min(roh, BREITE_MAX) : 320;
|
||||
});
|
||||
const ziehen = useRef(null);
|
||||
|
||||
function griffRunter(e) {
|
||||
e.preventDefault();
|
||||
ziehen.current = { startX: e.clientX, startBreite: listenBreite };
|
||||
window.addEventListener("pointermove", griffBewegt);
|
||||
window.addEventListener("pointerup", griffHoch);
|
||||
}
|
||||
function griffBewegt(e) {
|
||||
const z = ziehen.current;
|
||||
if (!z) return;
|
||||
const breite = Math.max(BREITE_MIN, Math.min(BREITE_MAX, z.startBreite + (e.clientX - z.startX)));
|
||||
setListenBreite(breite);
|
||||
}
|
||||
function griffHoch() {
|
||||
ziehen.current = null;
|
||||
window.removeEventListener("pointermove", griffBewegt);
|
||||
window.removeEventListener("pointerup", griffHoch);
|
||||
setListenBreite((b) => { try { localStorage.setItem(REZEPT_BREITE, String(b)); } catch { /* ignorieren */ } return b; });
|
||||
}
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
// Welche Zutat gerade von Hand zugeordnet wird (null = keine).
|
||||
const [zuordnen, setZuordnen] = useState(null);
|
||||
@@ -203,8 +236,8 @@ export default function Rezepte() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "grid", gap: "var(--sp-4)",
|
||||
gridTemplateColumns: "minmax(240px, 320px) 1fr" }}>
|
||||
<div style={{ display: "grid", gap: "var(--sp-2)", alignItems: "start",
|
||||
gridTemplateColumns: `${listenBreite}px auto 1fr` }}>
|
||||
<div className="card">
|
||||
<div className="card-head"><Icon name="food" /><h2>{liste.length} Rezepte</h2></div>
|
||||
{laedt && <p className="muted small">Wird geladen…</p>}
|
||||
@@ -215,7 +248,7 @@ export default function Rezepte() {
|
||||
{liste.map((r) => (
|
||||
<li key={r.slug}>
|
||||
<button type="button"
|
||||
className={`btn sm ghost${r.slug === slug ? " primary" : ""}`}
|
||||
className={`btn sm ghost umbruch${r.slug === slug ? " primary" : ""}`}
|
||||
style={{ width: "100%", justifyContent: "flex-start" }}
|
||||
onClick={() => oeffneRezept(r.slug)}>
|
||||
{r.name}
|
||||
@@ -225,6 +258,15 @@ export default function Rezepte() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="spalten-griff"
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Breite der Rezeptliste"
|
||||
title="Ziehen, um die Liste breiter zu machen"
|
||||
onPointerDown={griffRunter}
|
||||
/>
|
||||
|
||||
<div className="card">
|
||||
{!rezept && <p className="muted">Links ein Rezept wählen.</p>}
|
||||
{rezept && (
|
||||
|
||||
@@ -659,6 +659,21 @@ td select { width: auto; min-width: 0; max-width: 100%; }
|
||||
.dt-th.sorted .dt-th-label { color: var(--accent); }
|
||||
.dt-sort { font-size: 0.7em; flex: 0 0 auto; }
|
||||
.dt-resize { position: absolute; top: 0; right: 0; width: 8px; height: 100%; cursor: col-resize; touch-action: none; }
|
||||
|
||||
/* Griff zwischen zwei Spalten einer Seite (Rezepte: Liste | Detail). Gleiche
|
||||
Bedienung wie der Spaltengriff der Tabelle, nur senkrecht durchgehend. */
|
||||
.spalten-griff {
|
||||
align-self: stretch; width: 8px; border-radius: 999px;
|
||||
background: transparent; border: 0; padding: 0;
|
||||
cursor: col-resize; touch-action: none;
|
||||
}
|
||||
.spalten-griff:hover, .spalten-griff:focus-visible { background: var(--accent-soft); }
|
||||
|
||||
/* Ein Knopf, dessen Beschriftung umbrechen darf - .btn ist sonst nowrap und
|
||||
schneidet lange Namen ab. */
|
||||
.btn.umbruch {
|
||||
white-space: normal; text-align: left; height: auto; line-height: 1.35;
|
||||
}
|
||||
.dt-resize:hover { background: var(--accent-soft); }
|
||||
.dt-filter td { padding: 4px 6px; vertical-align: middle; }
|
||||
.dt-filter-cell { display: flex; gap: 2px; align-items: center; }
|
||||
|
||||
Reference in New Issue
Block a user