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:
+5
-3
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user