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:
Scarriffle
2026-10-16 16:48:43 +02:00
co-authored by Claude Opus 5
parent 8de222072c
commit 9f5d0a2310
2 changed files with 60 additions and 3 deletions
+45 -3
View File
@@ -22,6 +22,12 @@ import { fmt } from "../units";
const BASIS_KURZ = { gram: "g", milliliter: "ml", piece: "Stk" }; const BASIS_KURZ = { gram: "g", milliliter: "ml", piece: "Stk" };
const kurz = (basis) => BASIS_KURZ[basis] || basis || ""; 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 = { const ZUSTAND = {
vorhanden: { text: "vorhanden", klasse: "ok", icon: "check" }, vorhanden: { text: "vorhanden", klasse: "ok", icon: "check" },
zu_wenig: { text: "zu wenig", klasse: "warn", icon: "alert" }, zu_wenig: { text: "zu wenig", klasse: "warn", icon: "alert" },
@@ -54,6 +60,33 @@ export default function Rezepte() {
const [rezept, setRezept] = useState(null); const [rezept, setRezept] = useState(null);
const [portionen, setPortionen] = useState(urlPortionen); const [portionen, setPortionen] = useState(urlPortionen);
const [fehler, setFehler] = useState(null); 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); const [laedt, setLaedt] = useState(true);
// Welche Zutat gerade von Hand zugeordnet wird (null = keine). // Welche Zutat gerade von Hand zugeordnet wird (null = keine).
const [zuordnen, setZuordnen] = useState(null); const [zuordnen, setZuordnen] = useState(null);
@@ -203,8 +236,8 @@ export default function Rezepte() {
</form> </form>
</div> </div>
<div style={{ display: "grid", gap: "var(--sp-4)", <div style={{ display: "grid", gap: "var(--sp-2)", alignItems: "start",
gridTemplateColumns: "minmax(240px, 320px) 1fr" }}> gridTemplateColumns: `${listenBreite}px auto 1fr` }}>
<div className="card"> <div className="card">
<div className="card-head"><Icon name="food" /><h2>{liste.length} Rezepte</h2></div> <div className="card-head"><Icon name="food" /><h2>{liste.length} Rezepte</h2></div>
{laedt && <p className="muted small">Wird geladen…</p>} {laedt && <p className="muted small">Wird geladen…</p>}
@@ -215,7 +248,7 @@ export default function Rezepte() {
{liste.map((r) => ( {liste.map((r) => (
<li key={r.slug}> <li key={r.slug}>
<button type="button" <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" }} style={{ width: "100%", justifyContent: "flex-start" }}
onClick={() => oeffneRezept(r.slug)}> onClick={() => oeffneRezept(r.slug)}>
{r.name} {r.name}
@@ -225,6 +258,15 @@ export default function Rezepte() {
</ul> </ul>
</div> </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"> <div className="card">
{!rezept && <p className="muted">Links ein Rezept wählen.</p>} {!rezept && <p className="muted">Links ein Rezept wählen.</p>}
{rezept && ( {rezept && (
+15
View File
@@ -659,6 +659,21 @@ td select { width: auto; min-width: 0; max-width: 100%; }
.dt-th.sorted .dt-th-label { color: var(--accent); } .dt-th.sorted .dt-th-label { color: var(--accent); }
.dt-sort { font-size: 0.7em; flex: 0 0 auto; } .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; } .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-resize:hover { background: var(--accent-soft); }
.dt-filter td { padding: 4px 6px; vertical-align: middle; } .dt-filter td { padding: 4px 6px; vertical-align: middle; }
.dt-filter-cell { display: flex; gap: 2px; align-items: center; } .dt-filter-cell { display: flex; gap: 2px; align-items: center; }