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
+157 -89
View File
@@ -59,15 +59,17 @@ function parseScale(input) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Material-UI-Style: alle Pills auf einheitliche Hoehe (PILL_H) // Vectorworks-inspirierte Bar-Widgets: Icon links, Label/Select Mitte,
// nutzt globale Klassen aus index.css fuer Look (border-radius, colors) // 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 = { const sep = {
width: 1, height: 18, width: 1, height: 22,
background: 'var(--border)', flexShrink: 0, background: 'var(--border)', flexShrink: 0,
margin: '0 2px', margin: '0 4px',
} }
const groupLabel = { const groupLabel = {
fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase', fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase',
@@ -80,6 +82,71 @@ const pillSelect = {
padding: '0 20px 0 10px', boxSizing: 'border-box', padding: '0 20px 0 10px', boxSizing: 'border-box',
fontSize: 10, 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 = { const pillInput = {
height: PILL_H, lineHeight: PILL_H + 'px', height: PILL_H, lineHeight: PILL_H + 'px',
padding: '0 8px', boxSizing: 'border-box', padding: '0 8px', boxSizing: 'border-box',
@@ -265,87 +332,86 @@ export default function OberleisteApp() {
<Icon name="settings" size={14} /> <Icon name="settings" size={14} />
</button> </button>
<div style={sep} /> <div style={sep} />
{/* ====== GRUPPE: VIEW ====== */} {/* ====== VIEW (Top/Front/Right/Iso/Persp + Kamera) ======
<span style={groupLabel}>View</span> Kein Group-Label — die Buttons selber kommunizieren ihren Zweck. */}
{VIEWS.map(v => ( <div style={{ display: 'inline-flex', gap: 0,
<ToolButton border: '1px solid var(--border-light)', borderRadius: 4,
key={v.value} overflow: 'hidden', flexShrink: 0 }}>
onClick={() => setView(v.value)} {VIEWS.map((v, idx) => (
active={matchView(v.value)} <button
icon={v.icon} key={v.value}
label={v.label} onClick={() => setView(v.value)}
title={`Ansicht ${v.label}`} title={`Ansicht ${v.label}`}
/> style={{
))} height: BAR_H, padding: '0 8px',
<button background: matchView(v.value) ? 'var(--accent)' : 'var(--bg-input)',
onClick={() => openKameraPanel()} color: matchView(v.value) ? 'var(--bg-panel)' : 'var(--text-primary)',
title="Kamera-Einstellungen (Position, Target, Linse, Presets)" border: 'none',
className="btn-outlined" borderLeft: idx > 0 ? '1px solid var(--border-light)' : 'none',
style={{ height: PILL_H, padding: '0 6px', boxSizing: 'border-box', cursor: 'pointer',
fontSize: 9 }} display: 'flex', alignItems: 'center', gap: 4,
> fontSize: 10,
<Icon name="videocam" size={12} /> }}
</button> >
<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} /> <div style={sep} />
{/* ====== GRUPPE: DISPLAY-MODE ====== */} {/* ====== DISPLAY-MODE ====== */}
<span style={groupLabel}>Display</span> <BarSelect
<select icon="lightbulb"
value={state.displayMode || ''} value={state.displayMode || ''}
onChange={(e) => setDisplayMode(e.target.value)} onChange={(v) => setDisplayMode(v)}
style={pillSelect}
title="Display-Mode (Wireframe / Shaded / Rendered / etc.)" title="Display-Mode (Wireframe / Shaded / Rendered / etc.)"
width={160}
> >
{!state.displayMode && <option value=""></option>} {!state.displayMode && <option value=""></option>}
{(state.displayModes || []).map(dm => ( {(state.displayModes || []).map(dm => (
<option key={dm.id} value={dm.name}>{dm.name}</option> <option key={dm.id} value={dm.name}>{dm.name}</option>
))} ))}
</select> </BarSelect>
<div style={sep} /> <div style={sep} />
{/* ====== GRUPPE: MASSE ====== */} {/* ====== MASSE (Preset-Picker + Settings) ====== */}
<span style={groupLabel}>Masse</span> <BarSelect
<select icon="straighten"
value={state.masseActiveId || ''} value={state.masseActiveId || ''}
onChange={(e) => setMasseActive(e.target.value)} onChange={(v) => setMasseActive(v)}
style={pillSelect}
title="Aktives Mass — Raum-Rundung + Mass-Linien-Format" title="Aktives Mass — Raum-Rundung + Mass-Linien-Format"
width={140}
joinedRight
> >
{(state.massePresets || []).length === 0 && <option value=""></option>} {(state.massePresets || []).length === 0 && <option value=""></option>}
{(state.massePresets || []).map(p => ( {(state.massePresets || []).map(p => (
<option key={p.id} value={p.id}>{p.name}</option> <option key={p.id} value={p.id}>{p.name}</option>
))} ))}
</select> </BarSelect>
<button <BarButton icon="settings" onClick={() => openMasseSettings()}
onClick={() => openMasseSettings()} title="Masse bearbeiten / neues anlegen" joinedLeft />
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>
<div style={sep} /> <div style={sep} />
{/* ====== GRUPPE: MASSSTAB ====== */} {/* ====== MASSSTAB ====== */}
<span style={groupLabel}>Massstab</span> {/* Live-Zoom Chip mit Mass-Icon */}
<span <div style={{
className={isPerspective ? 'chip' : 'chip chip-accent'} display: 'inline-flex', alignItems: 'center',
style={{ height: BAR_H, padding: '0 10px',
...pillChip, background: isPerspective ? 'var(--bg-input)' : 'var(--accent)',
minWidth: 56, justifyContent: 'center', color: isPerspective ? 'var(--text-muted)' : 'var(--bg-panel)',
fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600, border: '1px solid var(--border-light)',
}} borderRadius: 4,
title="Live-Zoom" fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600,
> minWidth: 64, justifyContent: 'center', flexShrink: 0,
{/* Live-Zoom des Viewports — immer sichtbar bei Parallelprojektion, }} title="Live-Zoom">
unabhaengig davon ob ein Massstab im Dropdown gepinnt ist oder
nicht. Nur in Perspective ist die Anzeige nicht sinnvoll. */}
{isPerspective ? '—' : fmtScale(scaleVal)} {isPerspective ? '—' : fmtScale(scaleVal)}
</span> </div>
{customMode ? ( {customMode ? (
<input <input
ref={customInputRef} ref={customInputRef}
@@ -358,15 +424,25 @@ export default function OberleisteApp() {
else if (e.key === 'Escape') cancelDraft() else if (e.key === 'Escape') cancelDraft()
}} }}
onBlur={applyDraft} 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)" title="Massstab eingeben (Enter = uebernehmen, Esc = abbrechen)"
/> />
) : ( ) : (
<select <BarSelect
disabled={isPerspective} icon="straighten"
value={dropdownValue} value={dropdownValue}
onChange={(e) => applyDropdown(e.target.value)} onChange={(v) => applyDropdown(v)}
style={{ ...pillSelect, width: 92 }} disabled={isPerspective}
width={120}
title="Gesetzter Massstab"
> >
<option value="__none__"></option> <option value="__none__"></option>
{PRESETS.map(p => ( {PRESETS.map(p => (
@@ -376,30 +452,22 @@ export default function OberleisteApp() {
<option value={String(appliedScale)}>1:{appliedScale}</option> <option value={String(appliedScale)}>1:{appliedScale}</option>
)} )}
<option value="__custom__">Eigener</option> <option value="__custom__">Eigener</option>
</select> </BarSelect>
)} )}
<ToolButton <BarButton icon="percent" onClick={apply100}
onClick={apply100} disabled={isPerspective || !appliedScale}
disabled={isPerspective || !appliedScale} title={appliedScale ? `Zoom auf 1:${appliedScale} snappen` : 'Erst einen Massstab wählen'} />
label="100%" <BarButton icon="fit_screen" onClick={zoomExtents}
title={appliedScale ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` : 'Erst einen Massstab wählen'} title="Auf gesamten Inhalt zoomen" />
/> <BarButton icon="center_focus_strong" onClick={zoomSelection}
<button className="btn-icon" onClick={zoomExtents} title="Auf Selektion zoomen" />
style={pillIconBtn} <BarButton
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)'}
icon={state.showLineweights ? 'print' : 'edit'} 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} /> <div style={sep} />