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:
+150
-82
@@ -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,
|
||||||
|
overflow: 'hidden', flexShrink: 0 }}>
|
||||||
|
{VIEWS.map((v, idx) => (
|
||||||
|
<button
|
||||||
key={v.value}
|
key={v.value}
|
||||||
onClick={() => setView(v.value)}
|
onClick={() => setView(v.value)}
|
||||||
active={matchView(v.value)}
|
|
||||||
icon={v.icon}
|
|
||||||
label={v.label}
|
|
||||||
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} />
|
<Icon name={v.icon} size={13} />
|
||||||
|
<span>{v.label}</span>
|
||||||
</button>
|
</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)',
|
||||||
|
border: '1px solid var(--border-light)',
|
||||||
|
borderRadius: 4,
|
||||||
fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600,
|
fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600,
|
||||||
}}
|
minWidth: 64, justifyContent: 'center', flexShrink: 0,
|
||||||
title="Live-Zoom"
|
}} 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. */}
|
|
||||||
{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}
|
||||||
label="100%"
|
title={appliedScale ? `Zoom auf 1:${appliedScale} snappen` : 'Erst einen Massstab wählen'} />
|
||||||
title={appliedScale ? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})` : 'Erst einen Massstab wählen'}
|
<BarButton icon="fit_screen" onClick={zoomExtents}
|
||||||
/>
|
title="Auf gesamten Inhalt zoomen" />
|
||||||
<button className="btn-icon" onClick={zoomExtents}
|
<BarButton icon="center_focus_strong" onClick={zoomSelection}
|
||||||
style={pillIconBtn}
|
title="Auf Selektion zoomen" />
|
||||||
title="Auf gesamten Inhalt zoomen">
|
<BarButton
|
||||||
<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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user