Oberleiste Vectorworks-Style: Icon links + rechteckige Bar-Widgets

User-Referenz: Vectorworks Topbar. Drei Cues uebernommen:
- Icon-Kompartiment links im Dropdown (statt UPPERCASE Group-Label oben)
- Rechteckige Form mit dezenter Trennlinie zwischen Icon-Box und Inhalt
- Verbundene Widgets (Select + Settings-Gear) als eine visuelle Einheit

Neue Komponenten in OberleisteApp.jsx:
- BarSelect: [Icon | Native-Select | Caret] in einer 24px-Container-Box.
  appearance:none entfernt den nativen Pfeil, eigener arrow_drop_down rechts.
  joinedRight-Flag fuer nahtloses Anschliessen an einen BarButton.
- BarButton: quadratischer Icon-Button, joinedLeft kein doppelter Border,
  active=true → Accent-Background (fuer Toggles wie Print-View).

Migrierte Gruppen:
- View: 5 Toggle-Buttons in einem zusammenhaengenden Rahmen (kein Group-Label),
  selektierter Button accent-gefuellt analog VW Custom-View-Highlight
- Display: BarSelect (lightbulb icon)
- Masse: BarSelect joinedRight + BarButton joinedLeft (zahnrad)
- Massstab: Live-Zoom-Chip (eigene Box), BarSelect (straighten icon),
  BarButton (percent / fit_screen / center_focus_strong)
- Print/Edit-Toggle: BarButton mit active-State

Stack-Block (Overrides + Kombi) bleibt erst — separate Architektur,
nicht im Vectorworks-Vergleich enthalten. Migration spaeter wenn Bedarf.

PILL_H bleibt fuer nicht-migrierte Stellen, BAR_H = 24 fuer die neuen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 21:52:36 +02:00
parent 2ee4688fe3
commit c22aef6b65
+150 -82
View File
@@ -59,15 +59,17 @@ function parseScale(input) {
}
// ---------------------------------------------------------------------------
// Material-UI-Style: alle Pills auf einheitliche Hoehe (PILL_H)
// nutzt globale Klassen aus index.css fuer Look (border-radius, colors)
// Vectorworks-inspirierte Bar-Widgets: Icon links, Label/Select Mitte,
// Caret rechts — alle in einem rechteckigen Container, sichtbare Trennung
// zwischen Icon-Kompartiment und Inhalt.
const PILL_H = 20 // Einheits-Hoehe fuer alle Bar-Elemente
const PILL_H = 20 // alte Pill-Hoehe (Buttons/Chips die nicht migriert sind)
const BAR_H = 24 // neue Widget-Hoehe (BarSelect, BarButton, BarGroup)
const sep = {
width: 1, height: 18,
width: 1, height: 22,
background: 'var(--border)', flexShrink: 0,
margin: '0 2px',
margin: '0 4px',
}
const groupLabel = {
fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase',
@@ -80,6 +82,71 @@ const pillSelect = {
padding: '0 20px 0 10px', boxSizing: 'border-box',
fontSize: 10,
}
// BarSelect: Icon-Kompartiment links | Native-Select Mitte | Caret rechts
function BarSelect({ icon, value, onChange, title, disabled, width, children, joinedRight }) {
return (
<div title={title} style={{
display: 'inline-flex', alignItems: 'center',
height: BAR_H, width,
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: joinedRight ? '4px 0 0 4px' : 4,
borderRight: joinedRight ? 'none' : '1px solid var(--border-light)',
overflow: 'hidden', position: 'relative',
opacity: disabled ? 0.5 : 1, flexShrink: 0,
}}>
<div style={{
width: 26, height: '100%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'var(--bg-item)',
borderRight: '1px solid var(--border-light)',
flexShrink: 0,
}}>
<Icon name={icon} size={13} style={{ color: 'var(--text-muted)' }} />
</div>
<select
value={value || ''}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
style={{
flex: 1, height: '100%', minWidth: 0,
background: 'transparent', border: 'none', outline: 'none',
padding: '0 18px 0 8px',
fontSize: 11, color: 'var(--text-primary)',
appearance: 'none', WebkitAppearance: 'none', MozAppearance: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
}}
>{children}</select>
<Icon name="arrow_drop_down" size={14}
style={{ position: 'absolute', right: 4, top: '50%',
transform: 'translateY(-50%)',
color: 'var(--text-muted)', pointerEvents: 'none' }} />
</div>
)
}
// BarButton: quadratischer Icon-Button, gleiche Hoehe wie BarSelect.
// joinedLeft: wenn rechts von einem BarSelect sitzt (kein doppelter Border).
function BarButton({ icon, onClick, title, disabled, joinedLeft, active }) {
return (
<button onClick={onClick} disabled={disabled} title={title}
style={{
height: BAR_H, width: BAR_H,
background: active ? 'var(--accent)' : 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: joinedLeft ? '0 4px 4px 0' : 4,
borderLeft: joinedLeft ? 'none' : '1px solid var(--border-light)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1, flexShrink: 0,
padding: 0,
}}>
<Icon name={icon} size={13}
style={{ color: active ? 'var(--bg-panel)' : 'var(--text-muted)' }} />
</button>
)
}
const pillInput = {
height: PILL_H, lineHeight: PILL_H + 'px',
padding: '0 8px', boxSizing: 'border-box',
@@ -265,87 +332,86 @@ export default function OberleisteApp() {
<Icon name="settings" size={14} />
</button>
<div style={sep} />
{/* ====== GRUPPE: VIEW ====== */}
<span style={groupLabel}>View</span>
{VIEWS.map(v => (
<ToolButton
{/* ====== VIEW (Top/Front/Right/Iso/Persp + Kamera) ======
Kein Group-Label — die Buttons selber kommunizieren ihren Zweck. */}
<div style={{ display: 'inline-flex', gap: 0,
border: '1px solid var(--border-light)', borderRadius: 4,
overflow: 'hidden', flexShrink: 0 }}>
{VIEWS.map((v, idx) => (
<button
key={v.value}
onClick={() => setView(v.value)}
active={matchView(v.value)}
icon={v.icon}
label={v.label}
title={`Ansicht ${v.label}`}
/>
))}
<button
onClick={() => openKameraPanel()}
title="Kamera-Einstellungen (Position, Target, Linse, Presets)"
className="btn-outlined"
style={{ height: PILL_H, padding: '0 6px', boxSizing: 'border-box',
fontSize: 9 }}
style={{
height: BAR_H, padding: '0 8px',
background: matchView(v.value) ? 'var(--accent)' : 'var(--bg-input)',
color: matchView(v.value) ? 'var(--bg-panel)' : 'var(--text-primary)',
border: 'none',
borderLeft: idx > 0 ? '1px solid var(--border-light)' : 'none',
cursor: 'pointer',
display: 'flex', alignItems: 'center', gap: 4,
fontSize: 10,
}}
>
<Icon name="videocam" size={12} />
<Icon name={v.icon} size={13} />
<span>{v.label}</span>
</button>
))}
</div>
<BarButton icon="videocam" onClick={() => openKameraPanel()}
title="Kamera-Einstellungen (Position, Target, Linse, Presets)" />
<div style={sep} />
{/* ====== GRUPPE: DISPLAY-MODE ====== */}
<span style={groupLabel}>Display</span>
<select
{/* ====== DISPLAY-MODE ====== */}
<BarSelect
icon="lightbulb"
value={state.displayMode || ''}
onChange={(e) => setDisplayMode(e.target.value)}
style={pillSelect}
onChange={(v) => setDisplayMode(v)}
title="Display-Mode (Wireframe / Shaded / Rendered / etc.)"
width={160}
>
{!state.displayMode && <option value=""></option>}
{(state.displayModes || []).map(dm => (
<option key={dm.id} value={dm.name}>{dm.name}</option>
))}
</select>
</BarSelect>
<div style={sep} />
{/* ====== GRUPPE: MASSE ====== */}
<span style={groupLabel}>Masse</span>
<select
{/* ====== MASSE (Preset-Picker + Settings) ====== */}
<BarSelect
icon="straighten"
value={state.masseActiveId || ''}
onChange={(e) => setMasseActive(e.target.value)}
style={pillSelect}
onChange={(v) => setMasseActive(v)}
title="Aktives Mass — Raum-Rundung + Mass-Linien-Format"
width={140}
joinedRight
>
{(state.massePresets || []).length === 0 && <option value=""></option>}
{(state.massePresets || []).map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
<button
onClick={() => openMasseSettings()}
title="Masse bearbeiten / neues anlegen"
className="btn-outlined"
style={{ height: PILL_H, padding: '0 6px', boxSizing: 'border-box',
fontSize: 9 }}
>
<Icon name="settings" size={12} />
</button>
</BarSelect>
<BarButton icon="settings" onClick={() => openMasseSettings()}
title="Masse bearbeiten / neues anlegen" joinedLeft />
<div style={sep} />
{/* ====== GRUPPE: MASSSTAB ====== */}
<span style={groupLabel}>Massstab</span>
<span
className={isPerspective ? 'chip' : 'chip chip-accent'}
style={{
...pillChip,
minWidth: 56, justifyContent: 'center',
{/* ====== MASSSTAB ====== */}
{/* Live-Zoom Chip mit Mass-Icon */}
<div style={{
display: 'inline-flex', alignItems: 'center',
height: BAR_H, padding: '0 10px',
background: isPerspective ? 'var(--bg-input)' : 'var(--accent)',
color: isPerspective ? 'var(--text-muted)' : 'var(--bg-panel)',
border: '1px solid var(--border-light)',
borderRadius: 4,
fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600,
}}
title="Live-Zoom"
>
{/* Live-Zoom des Viewports — immer sichtbar bei Parallelprojektion,
unabhaengig davon ob ein Massstab im Dropdown gepinnt ist oder
nicht. Nur in Perspective ist die Anzeige nicht sinnvoll. */}
minWidth: 64, justifyContent: 'center', flexShrink: 0,
}} title="Live-Zoom">
{isPerspective ? '—' : fmtScale(scaleVal)}
</span>
</div>
{customMode ? (
<input
ref={customInputRef}
@@ -358,15 +424,25 @@ export default function OberleisteApp() {
else if (e.key === 'Escape') cancelDraft()
}}
onBlur={applyDraft}
style={{ ...pillInput, width: 92 }}
style={{
height: BAR_H, width: 100,
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: 4,
padding: '0 8px', fontSize: 11,
fontFamily: 'DM Mono, monospace',
color: 'var(--text-primary)', outline: 'none',
}}
title="Massstab eingeben (Enter = uebernehmen, Esc = abbrechen)"
/>
) : (
<select
disabled={isPerspective}
<BarSelect
icon="straighten"
value={dropdownValue}
onChange={(e) => applyDropdown(e.target.value)}
style={{ ...pillSelect, width: 92 }}
onChange={(v) => applyDropdown(v)}
disabled={isPerspective}
width={120}
title="Gesetzter Massstab"
>
<option value="__none__"></option>
{PRESETS.map(p => (
@@ -376,30 +452,22 @@ export default function OberleisteApp() {
<option value={String(appliedScale)}>1:{appliedScale}</option>
)}
<option value="__custom__">Eigener</option>
</select>
</BarSelect>
)}
<ToolButton
onClick={apply100}
<BarButton icon="percent" onClick={apply100}
disabled={isPerspective || !appliedScale}
label="100%"
title={appliedScale ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` : 'Erst einen Massstab wählen'}
/>
<button className="btn-icon" onClick={zoomExtents}
style={pillIconBtn}
title="Auf gesamten Inhalt zoomen">
<Icon name="fit_screen" size={14} />
</button>
<button className="btn-icon" onClick={zoomSelection}
style={pillIconBtn}
title="Auf Selektion zoomen">
<Icon name="center_focus_strong" size={14} />
</button>
<ToolButton
onClick={() => setShowLineweights(!state.showLineweights)}
active={state.showLineweights}
label={state.showLineweights ? 'Print' : 'Edit'}
title={state.showLineweights ? 'Print-View aktiv — klick zum Ausschalten' : 'Strichstärken anzeigen (Print-View)'}
title={appliedScale ? `Zoom auf 1:${appliedScale} snappen` : 'Erst einen Massstab wählen'} />
<BarButton icon="fit_screen" onClick={zoomExtents}
title="Auf gesamten Inhalt zoomen" />
<BarButton icon="center_focus_strong" onClick={zoomSelection}
title="Auf Selektion zoomen" />
<BarButton
icon={state.showLineweights ? 'print' : 'edit'}
active={state.showLineweights}
onClick={() => setShowLineweights(!state.showLineweights)}
title={state.showLineweights
? 'Print-View aktiv — klick zum Ausschalten'
: 'Strichstärken anzeigen (Print-View)'}
/>
<div style={sep} />