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:
+157
-89
@@ -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
|
||||
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 }}
|
||||
>
|
||||
<Icon name="videocam" size={12} />
|
||||
</button>
|
||||
{/* ====== 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)}
|
||||
title={`Ansicht ${v.label}`}
|
||||
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={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',
|
||||
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. */}
|
||||
{/* ====== 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,
|
||||
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}
|
||||
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)'}
|
||||
<BarButton icon="percent" onClick={apply100}
|
||||
disabled={isPerspective || !appliedScale}
|
||||
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} />
|
||||
|
||||
Reference in New Issue
Block a user