Fang-Optionen und Display/Print-Umschalter in die Footerbar

Der Linien-Modus (Display/Print) wandert aus der Oberleiste und die
Fang-Optionen aus der Werkzeug-Palette in die Statusleiste unten:

- StatusBar: segmentierter Display/Print-Umschalter (Stil wie der
  Renderer-Umschalter) plus Fang-Pill mit nach oben öffnendem Popover
  (Master-Schalter + Endpunkt/Mittelpunkt/Schnittpunkt/Kante/Raster/
  Ortho + Rastermass).
- TopBar: Linien-Modus-Gruppe entfernt.
- ToolsPanel: Fang-Abschnitt entfernt.
- App reicht lineMode/snap an die StatusBar durch.
This commit is contained in:
2026-07-03 20:08:03 +02:00
parent 658536029b
commit 2b6b99059d
5 changed files with 306 additions and 123 deletions
+210 -1
View File
@@ -8,7 +8,9 @@
// Rein gesteuert (controlled): alle Werte kommen aus App (Modell + View-State),
// keine eigene Logik. Bezeichner englisch, UI-Text/Kommentare deutsch.
import { useEffect, useRef, useState } from "react";
import type { Vec2 } from "../model/types";
import type { SnapSettings } from "../tools/types";
import { t } from "../i18n";
/** Plan-Renderer-Wahl, gespiegelt aus dem `?engine`-URL-Param (siehe PlanView). */
@@ -41,6 +43,199 @@ export interface StatusBarProps {
* Live-Remount der Renderer-Hooks).
*/
onRendererModeChange: (mode: RendererMode) => void;
/** Linien-Darstellung: „display" (Haarlinien) oder „print" (echte mm-Stärken). */
lineMode: "display" | "print";
/** Linien-Darstellung wechseln. */
onLineModeChange: (mode: "display" | "print") => void;
/** Nur im Grundriss sinnvoll — deaktiviert beide Segmente wenn `false`. */
lineModeEnabled: boolean;
/** Fang-Einstellungen (Werkzeug-übergreifend). */
snap: SnapSettings;
/** Fang-Einstellungen ändern. */
onSnapChange: (s: SnapSettings) => void;
}
/**
* Zweisegmentige Umschalt-Pille Display/Print für die Statusleiste — optisch
* an `.sb-renderer-toggle` angelehnt (gleiche Höhe/Radius), eigene Klasse
* `.sb-seg` weil sie generisch (nicht renderer-spezifisch) bleibt.
*/
function LineModeToggle({
lineMode,
onLineModeChange,
lineModeEnabled,
}: {
lineMode: "display" | "print";
onLineModeChange: (m: "display" | "print") => void;
lineModeEnabled: boolean;
}) {
return (
<span className="sb-field">
<span className="sb-key">{t("topbar.lineModeGroup")}</span>
<span
className={`sb-seg${lineModeEnabled ? "" : " disabled"}`}
role="group"
aria-label={t("topbar.lineModeGroup")}
>
<button
type="button"
className={`sb-seg-btn${lineMode === "display" ? " active" : ""}`}
aria-pressed={lineMode === "display"}
disabled={!lineModeEnabled}
title={t("topbar.lineMode.display.hint")}
onClick={() => onLineModeChange("display")}
>
{t("topbar.lineMode.display")}
</button>
<button
type="button"
className={`sb-seg-btn${lineMode === "print" ? " active" : ""}`}
aria-pressed={lineMode === "print"}
disabled={!lineModeEnabled}
title={t("topbar.lineMode.print.hint")}
onClick={() => onLineModeChange("print")}
>
{t("topbar.lineMode.print")}
</button>
</span>
</span>
);
}
/**
* Fang-Kontrolle für die Statusleiste: eine Pille schaltet `snap.enabled`
* direkt um (Klick = an/aus, aktive Einfärbung wie der Renderer-Umschalter);
* ein kleiner Caret daneben öffnet ein NACH OBEN öffnendes Popover (Leiste
* sitzt unten) mit den Fang-Unteroptionen — 1:1 aus dem früheren
* ToolsPanel-„Fang-Abschnitt" übernommen. Schliesst bei Klick ausserhalb.
*/
function SnapControl({
snap,
onSnapChange,
}: {
snap: SnapSettings;
onSnapChange: (s: SnapSettings) => void;
}) {
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLSpanElement | null>(null);
useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
setOpen(false);
}
};
document.addEventListener("mousedown", onDown);
return () => document.removeEventListener("mousedown", onDown);
}, [open]);
return (
<span className="sb-snap-wrap" ref={wrapRef}>
<span className={`sb-seg${open ? " open" : ""}`} role="group" aria-label={t("tool.snap")}>
<button
type="button"
className={`sb-seg-btn${snap.enabled ? " active" : ""}`}
aria-pressed={snap.enabled}
title={t("tool.snap.hint")}
onClick={() => onSnapChange({ ...snap, enabled: !snap.enabled })}
>
{t("tool.snap")}
</button>
<button
type="button"
className={`sb-seg-btn sb-seg-caret${open ? " active" : ""}`}
aria-haspopup="true"
aria-expanded={open}
aria-label={t("tool.snap")}
title={t("tool.snap")}
onClick={() => setOpen((v) => !v)}
>
</button>
</span>
{open && (
<div className="sb-snap-popover" role="dialog" aria-label={t("tool.snap")}>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.enabled}
onChange={(e) => onSnapChange({ ...snap, enabled: e.target.checked })}
/>
<span>{t("snap.enabled")}</span>
</label>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.endpoint}
disabled={!snap.enabled}
onChange={(e) => onSnapChange({ ...snap, endpoint: e.target.checked })}
/>
<span>{t("snap.endpoint")}</span>
</label>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.midpoint}
disabled={!snap.enabled}
onChange={(e) => onSnapChange({ ...snap, midpoint: e.target.checked })}
/>
<span>{t("snap.midpoint")}</span>
</label>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.intersection}
disabled={!snap.enabled}
onChange={(e) =>
onSnapChange({ ...snap, intersection: e.target.checked })
}
/>
<span>{t("snap.intersection")}</span>
</label>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.onEdge}
disabled={!snap.enabled}
onChange={(e) => onSnapChange({ ...snap, onEdge: e.target.checked })}
/>
<span>{t("snap.onEdge")}</span>
</label>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.grid}
disabled={!snap.enabled}
onChange={(e) => onSnapChange({ ...snap, grid: e.target.checked })}
/>
<span>{t("snap.grid")}</span>
</label>
<label className="sb-snap-row">
<input
type="checkbox"
checked={snap.ortho}
disabled={!snap.enabled}
onChange={(e) => onSnapChange({ ...snap, ortho: e.target.checked })}
/>
<span>{t("snap.ortho")}</span>
</label>
<label className="sb-snap-row sb-snap-row-num">
<span>{t("snap.gridSize")}</span>
<input
type="number"
step={0.05}
min={0.01}
value={snap.gridSize}
onChange={(e) =>
onSnapChange({ ...snap, gridSize: Number(e.target.value) })
}
/>
</label>
</div>
)}
</span>
);
}
/** Ein Wert-Feld im rechten Cluster: kleine Beschriftung + Wert. */
@@ -72,6 +267,11 @@ export function StatusBar({
rendererMode,
webGpuAvailable,
onRendererModeChange,
lineMode,
onLineModeChange,
lineModeEnabled,
snap,
onSnapChange,
}: StatusBarProps) {
const coords = cursor
? `${fmt(cursor.x)}, ${fmt(cursor.y)}`
@@ -82,8 +282,17 @@ export function StatusBar({
{/* Links: Werkzeug-Hinweis. */}
<span className="sb-hint">{hint}</span>
{/* Rechts: abgeleiteter Wert-Cluster. */}
{/* Rechts: abgeleiteter Wert-Cluster. Interaktive Kontrollen (Linien-
Darstellung, Fang) zuerst, danach die reinen Anzeigewerte. */}
<div className="sb-cluster">
<LineModeToggle
lineMode={lineMode}
onLineModeChange={onLineModeChange}
lineModeEnabled={lineModeEnabled}
/>
<Sep />
<SnapControl snap={snap} onSnapChange={onSnapChange} />
<Sep />
<span className="sb-field">
<span className="sb-key">{t("status.renderer")}</span>
<span className="sb-renderer-toggle" role="group" aria-label={t("status.renderer")}>
-33
View File
@@ -141,12 +141,6 @@ export interface TopBarProps {
/** Referenzlinien nur im Grundriss sinnvoll (sonst deaktiviert). */
referenceLinesEnabled: boolean;
// Linien-Darstellung: Display (Haarlinien) ↔ Print (echte mm-Strichstärken).
lineMode: "display" | "print";
onLineMode: (m: "display" | "print") => void;
/** Nur im Grundriss sinnvoll. */
lineModeEnabled: boolean;
// Sichtbarkeits-Kombinationen (Ebenen-/Zeichnungskombinationen).
/** Handler-Satz für die beiden Kombinations-Dropdowns. */
combos: ComboMenuHandlers;
@@ -855,9 +849,6 @@ export function TopBar({
referenceLines,
onReferenceLines,
referenceLinesEnabled,
lineMode,
onLineMode,
lineModeEnabled,
combos,
resourcesOpen,
onToggleResources,
@@ -1202,30 +1193,6 @@ export function TopBar({
<Sep />
{/* Linien-Darstellung: Display (alle Haarlinien) ↔ Print (echte mm-
Strichstärken). Eine Einfach-Auswahl → als Dropdown (Trigger-Pill +
Häkchen-Liste, wie Detailgrad/Massstab), nicht mehr als Segment-Pillen.
Optionen tragen ein Icon plus Klartext. Nur im Grundriss sinnvoll. */}
<div className="tb-group" role="group" aria-label={t("topbar.lineModeGroup")}>
<label className="tb-field">
<span className="tb-label">{t("topbar.lineModeGroup")}</span>
<Dropdown
value={lineMode}
disabled={!lineModeEnabled}
onChange={(v) => onLineMode(v as "display" | "print")}
title={
lineModeEnabled
? `${t("topbar.lineMode.display.hint")} · ${t("topbar.lineMode.print.hint")}`
: t("topbar.lineModeGroup")
}
options={[
{ value: "display", label: t("topbar.lineMode.display") },
{ value: "print", label: t("topbar.lineMode.print") },
]}
/>
</label>
</div>
{/* Rechte Gruppe: Layout-Menü · Ressourcen · Projektname. */}
<div className="topbar-right">
{layoutMenu}