Oberleiste: Icon roh + Pill-Dropdowns, About-Modal, Overrides/Kombi migriert

User-Feedback: Icon-Box war zu plakativ, lieber roh daneben + Pill-Form.
Versionsnummer raus, Logo-Klick → About-Fenster.

BarSelect umgebaut:
- Icon roh links (vorher: separates Icon-Kompartiment mit Border)
- Select-Container pill-foermig (border-radius 999, vorher 4)
- joinedRight wird zu flachen rechten Pill-Kanten (statt rechteck)

BarButton: pill-foermig + joinedLeft mit flacher linker Kante.

View-Toggle: Pill-Container statt Rechteck.
Massstab Live-Zoom-Chip: Pill-Form.
Custom-Scale-Input: Pill-Form.

Logo:
- Versionsnummer-Span entfernt
- ganzes Logo wird zu Klick-Button → AboutModal-State
- AboutModal: zentrierter Backdrop-Dialog mit Launcher/Plugin-Versionen,
  Autor (Karim Gabriele Varano), Website (gabrielevarano.ch),
  Lizenz (Proprietaer)

Overrides + Kombi migriert (war noch im alten Stack-Layout):
- Overrides: BarButton (Toggle) + BarSelect (Preset) joinedRight + BarButton
  (Settings) joinedLeft, palette-Icon fuer Preset-Picker
- Kombi: BarSelect (layers-Icon) mit allen Aktionen im Dropdown
  (Speichern, Loeschen, Bearbeiten) + BarButton (edit) joinedLeft
- Stack-Layout (label-Spalte + 2 Reihen) ist weg, jetzt inline

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 22:00:07 +02:00
parent c22aef6b65
commit 76572968ca
+210 -158
View File
@@ -83,51 +83,53 @@ const pillSelect = {
fontSize: 10,
}
// BarSelect: Icon-Kompartiment links | Native-Select Mitte | Caret rechts
// BarSelect: Icon roh links (kein Container) + pill-shaped Native-Select.
// joinedRight: rechte Pill-Kante flach, fuer Verkettung mit BarButton.
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',
display: 'inline-flex', alignItems: 'center', gap: 5,
opacity: disabled ? 0.5 : 1, flexShrink: 0,
}}>
{icon && (
<Icon name={icon} size={13}
style={{ color: 'var(--text-muted)', 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,
position: 'relative',
height: BAR_H, width,
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderTopLeftRadius: 999, borderBottomLeftRadius: 999,
borderTopRightRadius: joinedRight ? 0 : 999,
borderBottomRightRadius: joinedRight ? 0 : 999,
borderRight: joinedRight ? 'none' : '1px solid var(--border-light)',
overflow: 'hidden', flexShrink: 0,
}}>
<Icon name={icon} size={13} style={{ color: 'var(--text-muted)' }} />
<select
value={value || ''}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
style={{
width: '100%', height: '100%', minWidth: 0,
background: 'transparent', border: 'none', outline: 'none',
padding: '0 22px 0 12px',
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: 6, top: '50%',
transform: 'translateY(-50%)',
color: 'var(--text-muted)', pointerEvents: 'none' }} />
</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).
// BarButton: pill-foermiger Icon-Button. joinedLeft = linke Kante flach,
// so dass er nahtlos an einen BarSelect-joinedRight andockt.
function BarButton({ icon, onClick, title, disabled, joinedLeft, active }) {
return (
<button onClick={onClick} disabled={disabled} title={title}
@@ -135,7 +137,9 @@ function BarButton({ icon, onClick, title, disabled, joinedLeft, active }) {
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,
borderTopLeftRadius: joinedLeft ? 0 : 999,
borderBottomLeftRadius: joinedLeft ? 0 : 999,
borderTopRightRadius: 999, borderBottomRightRadius: 999,
borderLeft: joinedLeft ? 'none' : '1px solid var(--border-light)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
@@ -204,6 +208,7 @@ export default function OberleisteApp() {
const [draft, setDraft] = useState('')
const [customMode, setCustomMode] = useState(false) // Dropdown -> Custom-Input switch
const customInputRef = useRef(null)
const [aboutOpen, setAboutOpen] = useState(false)
useEffect(() => {
onMessage('STATE', (s) => {
@@ -291,13 +296,16 @@ export default function OberleisteApp() {
overflowX: 'auto', overflowY: 'hidden',
flexShrink: 0,
}}>
{/* Logo: DOSSIER. (Petrol-Punkt) + Launcher-Version */}
<div
{/* Logo: DOSSIER. (Petrol-Punkt) — Klick = About-Fenster */}
<button
onClick={() => setAboutOpen(true)}
title="Über Dossier"
style={{
display: 'flex', alignItems: 'baseline', gap: 8,
flexShrink: 0, userSelect: 'none',
background: 'transparent', border: 'none', padding: 0,
cursor: 'pointer', color: 'inherit',
}}
title={`Dossier ${__LAUNCHER_VERSION__} (Plugin ${__APP_VERSION__}) — Teil von OpenStudio`}
>
<span style={{
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
@@ -308,16 +316,7 @@ export default function OberleisteApp() {
}}>
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
</span>
<span style={{
fontFamily: 'DM Mono, monospace',
fontSize: 9,
letterSpacing: '0.14em',
color: 'var(--text-muted)',
textTransform: 'uppercase',
}}>
v{__LAUNCHER_VERSION__}
</span>
</div>
</button>
<button
onClick={() => openDossierSettings()}
title="Dossier-Einstellungen"
@@ -333,9 +332,9 @@ export default function OberleisteApp() {
</button>
<div style={sep} />
{/* ====== VIEW (Top/Front/Right/Iso/Persp + Kamera) ======
Kein Group-Label — die Buttons selber kommunizieren ihren Zweck. */}
Pill-foermige Toggle-Gruppe, kein Group-Label. */}
<div style={{ display: 'inline-flex', gap: 0,
border: '1px solid var(--border-light)', borderRadius: 4,
border: '1px solid var(--border-light)', borderRadius: 999,
overflow: 'hidden', flexShrink: 0 }}>
{VIEWS.map((v, idx) => (
<button
@@ -343,7 +342,7 @@ export default function OberleisteApp() {
onClick={() => setView(v.value)}
title={`Ansicht ${v.label}`}
style={{
height: BAR_H, padding: '0 8px',
height: BAR_H, padding: '0 10px',
background: matchView(v.value) ? 'var(--accent)' : 'var(--bg-input)',
color: matchView(v.value) ? 'var(--bg-panel)' : 'var(--text-primary)',
border: 'none',
@@ -399,14 +398,14 @@ export default function OberleisteApp() {
<div style={sep} />
{/* ====== MASSSTAB ====== */}
{/* Live-Zoom Chip mit Mass-Icon */}
{/* Live-Zoom Chip — pill, accent wenn Massstab anwendbar */}
<div style={{
display: 'inline-flex', alignItems: 'center',
height: BAR_H, padding: '0 10px',
height: BAR_H, padding: '0 12px',
background: isPerspective ? 'var(--bg-input)' : 'var(--accent)',
color: isPerspective ? 'var(--text-muted)' : 'var(--bg-panel)',
border: '1px solid var(--border-light)',
borderRadius: 4,
borderRadius: 999,
fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600,
minWidth: 64, justifyContent: 'center', flexShrink: 0,
}} title="Live-Zoom">
@@ -428,8 +427,8 @@ export default function OberleisteApp() {
height: BAR_H, width: 100,
background: 'var(--bg-input)',
border: '1px solid var(--border-light)',
borderRadius: 4,
padding: '0 8px', fontSize: 11,
borderRadius: 999,
padding: '0 12px', fontSize: 11,
fontFamily: 'DM Mono, monospace',
color: 'var(--text-primary)', outline: 'none',
}}
@@ -478,113 +477,166 @@ export default function OberleisteApp() {
{/* ====== STACK: Overrides + Kombi uebereinander ======
Beide Zeilen haben identisches Spalten-Layout (Label-Spalte fix,
Dropdown gleich breit), damit Dropdowns vertikal aligned sind. */}
{(() => {
const STACK_LABEL_W = 60 // gleich breit fuer beide Zeilen
const STACK_DROPDOWN_W = 150
const stackLabel = { ...groupLabel, width: STACK_LABEL_W,
padding: 0, textAlign: 'left' }
return (
<div style={{
display: 'flex', flexDirection: 'column', gap: 4,
flexShrink: 0,
}}>
{/* Overrides */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={stackLabel}>Overrides</span>
<ToolButton
onClick={() => toggleOverrides(!state.overridesEnabled)}
active={state.overridesEnabled}
icon="auto_fix_high"
label={state.overridesEnabled ? 'AN' : 'AUS'}
title={state.overridesEnabled
? `Grafische Overrides aktiv — klick zum Ausschalten`
: `Grafische Overrides ausgeschaltet`}
/>
<select
value={state.overridesActivePreset || '__none__'}
onChange={(e) => {
const v = e.target.value
if (v === '__configure__') { openOverridesPanel(); return }
setOverridesPreset(v === '__none__' ? null : v)
}}
style={{ ...pillSelect, width: STACK_DROPDOWN_W }}
title={state.overridesActivePreset
? `Aktives Preset: ${state.overridesActivePreset} (${state.overridesCount} Regeln)`
: `Kein Preset aktiv (${state.overridesCount} Regeln, frei editiert)`}
>
<option value="__none__">{state.overridesCount > 0 ? `— (${state.overridesCount} Regeln)` : '—'}</option>
{(state.overridesPresets || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
<option disabled></option>
<option value="__configure__">Konfigurieren</option>
</select>
<ToolButton
onClick={openOverridesPanel}
icon="settings"
title="Overrides-Regel-Editor öffnen"
/>
</div>
{/* Kombi */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={stackLabel}>Kombi</span>
<ToolButton
onClick={() => {
const suggested = state.layerCombinationActive
|| `Kombi ${(state.layerCombinations || []).length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if ((state.layerCombinations || []).includes(name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveLayerCombination(name)
}}
icon="add"
title="Aktuelle Sichtbarkeit als neue Kombination speichern"
/>
<select
value={state.layerCombinationActive || '__none__'}
onChange={(e) => {
const v = e.target.value
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__delete__') {
if (state.layerCombinationActive &&
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
deleteLayerCombination(state.layerCombinationActive)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
style={{ ...pillSelect, width: STACK_DROPDOWN_W }}
title={state.layerCombinationActive
? `Aktive Kombi: ${state.layerCombinationActive}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
>
<option value="__none__"> Eigene </option>
{(state.layerCombinations || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
{state.layerCombinationActive && (
<>
<option disabled></option>
<option value="__delete__">🗑 Aktuelle löschen</option>
</>
)}
<option disabled></option>
<option value="__configure__">Bearbeiten</option>
</select>
<ToolButton
onClick={openLayerCombinationsDialog}
icon="edit"
title="Ebenenkombinationen bearbeiten"
/>
</div>
</div>
)
})()}
{/* ====== OVERRIDES ====== */}
<BarButton
icon="auto_fix_high"
active={state.overridesEnabled}
onClick={() => toggleOverrides(!state.overridesEnabled)}
title={state.overridesEnabled
? 'Grafische Overrides aktiv — klick zum Ausschalten'
: 'Grafische Overrides ausgeschaltet'}
/>
<BarSelect
icon="palette"
value={state.overridesActivePreset || '__none__'}
onChange={(v) => {
if (v === '__configure__') { openOverridesPanel(); return }
setOverridesPreset(v === '__none__' ? null : v)
}}
title={state.overridesActivePreset
? `Aktives Preset: ${state.overridesActivePreset} (${state.overridesCount} Regeln)`
: `Kein Preset aktiv (${state.overridesCount} Regeln, frei editiert)`}
width={140}
joinedRight
>
<option value="__none__">{state.overridesCount > 0 ? `— (${state.overridesCount} Regeln)` : '—'}</option>
{(state.overridesPresets || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
<option disabled></option>
<option value="__configure__">Konfigurieren</option>
</BarSelect>
<BarButton icon="settings" onClick={openOverridesPanel}
title="Overrides-Regel-Editor öffnen" joinedLeft />
<div style={sep} />
{/* ====== EBENEN-KOMBINATIONEN ====== */}
<BarSelect
icon="layers"
value={state.layerCombinationActive || '__none__'}
onChange={(v) => {
if (v === '__configure__') { openLayerCombinationsDialog(); return }
if (v === '__save__') {
const suggested = state.layerCombinationActive
|| `Kombi ${(state.layerCombinations || []).length + 1}`
const name = (window.prompt('Name für Ebenenkombination:', suggested) || '').trim()
if (!name) return
if ((state.layerCombinations || []).includes(name) &&
!window.confirm(`"${name}" überschreiben?`)) return
saveLayerCombination(name)
return
}
if (v === '__delete__') {
if (state.layerCombinationActive &&
window.confirm(`Kombination "${state.layerCombinationActive}" löschen?`))
deleteLayerCombination(state.layerCombinationActive)
return
}
pickLayerCombination(v === '__none__' ? null : v)
}}
title={state.layerCombinationActive
? `Aktive Kombi: ${state.layerCombinationActive}`
: 'Keine Kombination — manuelle Sichtbarkeit'}
width={150}
joinedRight
>
<option value="__none__"> Eigene </option>
{(state.layerCombinations || []).map(name => (
<option key={name} value={name}>{name}</option>
))}
<option disabled></option>
<option value="__save__">+ Aktuelle speichern</option>
{state.layerCombinationActive && (
<option value="__delete__">🗑 Aktuelle löschen</option>
)}
<option value="__configure__">Bearbeiten</option>
</BarSelect>
<BarButton icon="edit" onClick={openLayerCombinationsDialog}
title="Ebenenkombinationen bearbeiten" joinedLeft />
{/* Spacer am rechten Rand */}
<div style={{ flex: 1 }} />
</div>
{aboutOpen && (
<AboutModal onClose={() => setAboutOpen(false)} />
)}
</div>
)
}
function AboutModal({ onClose }) {
return (
<div
onClick={onClose}
style={{
position: 'fixed', inset: 0, zIndex: 1000,
background: 'rgba(0, 0, 0, 0.6)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
minWidth: 360, maxWidth: 420,
background: 'var(--bg-panel)',
border: '1px solid var(--border)',
borderRadius: 8,
padding: '24px 28px',
boxShadow: '0 12px 48px rgba(0, 0, 0, 0.5)',
color: 'var(--text-primary)',
fontFamily: 'var(--font)',
}}
>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
marginBottom: 4 }}>
<span style={{
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
fontSize: 28, letterSpacing: '-0.02em', lineHeight: 1,
}}>
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
</span>
</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)',
letterSpacing: '0.06em', textTransform: 'uppercase',
marginBottom: 18 }}>
Teil von OpenStudio
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
gap: '6px 14px', fontSize: 11, marginBottom: 18 }}>
<span style={{ color: 'var(--text-muted)' }}>Launcher</span>
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__LAUNCHER_VERSION__}</span>
<span style={{ color: 'var(--text-muted)' }}>Plugin</span>
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__APP_VERSION__}</span>
<span style={{ color: 'var(--text-muted)' }}>Autor</span>
<span>Karim Gabriele Varano</span>
<span style={{ color: 'var(--text-muted)' }}>Website</span>
<a href="https://gabrielevarano.ch" target="_blank" rel="noreferrer"
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
gabrielevarano.ch
</a>
<span style={{ color: 'var(--text-muted)' }}>Lizenz</span>
<span>Proprietär © 2026 Karim Gabriele Varano</span>
</div>
<div style={{ fontSize: 10, color: 'var(--text-muted)',
lineHeight: 1.5, marginBottom: 18,
paddingTop: 12,
borderTop: '1px solid var(--border-light)' }}>
Rhino 8 Plugin für architektonische Workflows Wände, Decken,
Öffnungen, Räume, SIA 416, Plan-Layouts. Schwester-App: Rapport.
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button
onClick={onClose}
className="btn-contained"
style={{ padding: '6px 18px', fontSize: 11 }}
>Schliessen</button>
</div>
</div>
</div>
)
}