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 694e666160
commit 5496f8c007
5 changed files with 306 additions and 123 deletions
+5 -3
View File
@@ -2715,9 +2715,6 @@ export default function App() {
referenceLines={referenceLines}
onReferenceLines={setReferenceLines}
referenceLinesEnabled={planActive}
lineMode={lineMode}
onLineMode={setLineMode}
lineModeEnabled={planActive}
combos={{
// Ebenen-Kombinationen: Speichern = Snapshot des Layer-Baums →
// localStorage; Laden = Map lesen → Sichtbarkeits-Flags anwenden.
@@ -2959,6 +2956,11 @@ export default function App() {
rendererMode={rendererMode}
webGpuAvailable={webGpuAvailable}
onRendererModeChange={setRendererMode}
lineMode={lineMode}
onLineModeChange={setLineMode}
lineModeEnabled={planActive}
snap={snap}
onSnapChange={setSnap}
/>
{/* Schwebendes Ressourcen-Fenster ÜBER allem (hoher z-index, abgedunkelter
-86
View File
@@ -119,7 +119,6 @@ function ToolIcon({ id }: { id: ToolId }) {
export function ToolsPanel() {
const host = usePanelHost();
const snap = host.snap;
return (
<div className="tools-panel">
@@ -190,91 +189,6 @@ export function ToolsPanel() {
)}
{host.activeTool === "ceiling" && <div className="tools-sep" />}
{/* Fang-Abschnitt. Master-Checkbox steuert die übrigen Fang-Optionen. */}
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, enabled: e.target.checked })}
/>
<span>{t("snap.enabled")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.endpoint}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, endpoint: e.target.checked })}
/>
<span>{t("snap.endpoint")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.midpoint}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, midpoint: e.target.checked })}
/>
<span>{t("snap.midpoint")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.intersection}
disabled={!snap.enabled}
onChange={(e) =>
host.onSnapChange({ ...snap, intersection: e.target.checked })
}
/>
<span>{t("snap.intersection")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.onEdge}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, onEdge: e.target.checked })}
/>
<span>{t("snap.onEdge")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.grid}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, grid: e.target.checked })}
/>
<span>{t("snap.grid")}</span>
</label>
<label className="tools-snap-row">
<input
type="checkbox"
checked={snap.ortho}
disabled={!snap.enabled}
onChange={(e) => host.onSnapChange({ ...snap, ortho: e.target.checked })}
/>
<span>{t("snap.ortho")}</span>
</label>
<label className="tools-field">
<span>{t("snap.gridSize")}</span>
<input
type="number"
step={0.05}
min={0.01}
value={snap.gridSize}
onChange={(e) =>
host.onSnapChange({ ...snap, gridSize: Number(e.target.value) })
}
/>
</label>
</div>
);
}
+91
View File
@@ -803,6 +803,97 @@ body {
cursor: default;
}
/* Generische Segment-Pille für die Statusleiste (Linien-Darstellung, Fang)
gleiche Masse/Optik wie .sb-renderer-toggle, aber nicht renderer-spezifisch. */
.sb-seg {
display: inline-flex;
align-items: stretch;
height: 16px;
border: 1px solid var(--border);
border-radius: 999px;
overflow: hidden;
background: var(--input);
}
.sb-seg.disabled {
opacity: 0.4;
}
.sb-seg-btn {
display: inline-flex;
align-items: center;
border: none;
border-left: 1px solid var(--border);
background: transparent;
color: var(--muted);
font-family: inherit;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 0 8px;
cursor: pointer;
transition: background 0.14s, color 0.14s;
}
.sb-seg-btn:first-child {
border-left: none;
}
.sb-seg-btn:hover:not(:disabled) {
background: var(--accent-dim);
color: var(--accent);
}
.sb-seg-btn.active {
background: var(--accent);
color: #fff;
}
.sb-seg-btn:disabled {
opacity: 0.4;
cursor: default;
}
/* Caret-Segment (öffnet das Fang-Popover) — schmaler als die Text-Segmente. */
.sb-seg-caret {
padding: 0 6px;
font-size: 9px;
line-height: 1;
}
/* Fang-Kontrolle: Wrapper ist relativ positioniert, damit das Popover NACH
OBEN öffnen kann (die Leiste sitzt ganz unten im App-Shell). */
.sb-snap-wrap {
position: relative;
display: inline-flex;
}
.sb-snap-popover {
position: absolute;
right: 0;
bottom: 100%;
margin: 0 0 6px;
padding: 6px 0;
min-width: 170px;
background: var(--panel, #1c2128);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
z-index: 200;
}
.sb-snap-row {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 12px;
font-size: 12px;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.sb-snap-row input[type="checkbox"] {
accent-color: var(--accent);
}
.sb-snap-row-num {
justify-content: space-between;
cursor: default;
}
.sb-snap-row-num input[type="number"] {
width: 64px;
}
/* Command-Line (Rhino-artig) über der Statusleiste
Dunkle, ruhige Leiste mit Prompt links, klickbaren Inline-Optionen und einer
monospace-Texteingabe rechts; das Autocomplete-Dropdown öffnet nach oben. */
+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}