UI-Konsistenz: shared BarControls + Tabellen-Look fuer Panels

Pill-basierte Toolbar-Primitiven aus OberleisteApp extrahiert nach
src/components/BarControls.jsx — BarCombo (Dropdown), BarButton
(Icon-Button), BarToggle (Label/Icon mit Active-State), BAR_H=22.

OberleisteApp nutzt jetzt die geteilten Komponenten (Verhalten
unveraendert).

EbenenManager + GeschossManager:
- Sichtbarkeits-Toolbar: native <select> + btn-icon-sm → BarCombo
  (mit visibility-Icon links) + BarButton add/settings.
- GeschossManager Stift-Icon (edit) → Settings-Icon.
- Zeilen-Layout: eckig statt Pill (margin 0, borderRadius 0,
  3px Accent-Strip links fuer aktive Zeile), minHeight 24, gap 4,
  kompaktere Padding/Icon-Sizes — Vectorworks-naeher.

DimensionenApp:
- Welt/CPlane: 2x BarToggle statt btn-contained/outlined
- Z-Selektor: 3x BarToggle (icon-only)
- Drehen-Apply + 90°-CCW/CW: BarButton mit rotate_*-Icons (4
  Preset-Buttons -90/-45/45/90 ersetzt durch 2 schnelle 90°-Buttons
  — passt besser in die schmale Sidebar)

README aktualisiert: Runtime jetzt CPython 3.9, TextEntity-RTF-Limit
dokumentiert, BarControls + text_editor/text_create erwaehnt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 23:56:33 +02:00
parent 26c7d9e67d
commit d5bcee2157
6 changed files with 280 additions and 233 deletions
+169
View File
@@ -0,0 +1,169 @@
import Icon from './Icon'
// Gemeinsame Toolbar-Primitiven für Panels im Oberleiste-Stil:
// Pill-Container mit konsistenter Höhe, Accent-Border bei Hover/Active.
// Quelle: ursprünglich in OberleisteApp.jsx — zur Wiederverwendung in
// weiteren Panels extrahiert.
export const BAR_H = 22
// BarCombo: dunklerer (bg-input) Pill-Container der select + optional gear
// als EINE nahtlose Box rendert. Icon roh links daneben (kein Container).
// iconClickable=true macht das Icon zum Toggle-Button.
// valueAccent=true faerbt den Select-Text accent.
export function BarCombo({
icon, iconActive, iconClickable, onIconClick, iconTitle,
value, onChange, width, title, children, disabled,
onGear, gearTitle, valueAccent,
}) {
return (
<div style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
opacity: disabled ? 0.5 : 1, flexShrink: 0,
}}>
{icon && (iconClickable ? (
<button onClick={onIconClick} title={iconTitle}
style={{
width: 18, height: BAR_H,
background: 'transparent', border: 'none',
cursor: 'pointer', flexShrink: 0, padding: 0,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
}}>
<Icon name={icon} size={13}
style={{ color: iconActive ? 'var(--accent)' : 'var(--text-muted)' }} />
</button>
) : (
<span style={{
width: 18, height: BAR_H, flexShrink: 0,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
}}>
<Icon name={icon} size={13} style={{ color: 'var(--text-muted)' }} />
</span>
))}
<div title={title}
onMouseEnter={(e) => {
if (disabled) return
e.currentTarget.style.borderColor = 'var(--accent)'
e.currentTarget.style.background = 'var(--bg-item-hover)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.background = 'var(--bg-input)'
}}
style={{
display: 'inline-flex', alignItems: 'stretch',
height: BAR_H + 2, width, boxSizing: 'border-box',
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999,
overflow: 'hidden',
transition: 'border-color 0.15s, background 0.15s',
}}>
<select
value={value || ''}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
style={{
flex: 1, minWidth: 0,
background: 'transparent',
color: valueAccent ? 'var(--accent-light)' : 'var(--text-primary)',
border: 'none', outline: 'none',
padding: '0 22px 0 12px',
fontSize: 11, fontFamily: 'var(--font)',
fontWeight: valueAccent ? 600 : 500,
appearance: 'none', WebkitAppearance: 'none',
backgroundImage: 'var(--select-arrow)',
backgroundRepeat: 'no-repeat',
backgroundPosition: onGear ? 'right 1px center' : 'right 10px center',
cursor: disabled ? 'not-allowed' : 'pointer',
letterSpacing: 0,
}}
>{children}</select>
{onGear && (
<button onClick={onGear} title={gearTitle}
style={{
background: 'transparent', border: 'none',
padding: '0 8px', cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0,
}}>
<Icon name="settings" size={12}
style={{ color: 'var(--text-muted)' }} />
</button>
)}
</div>
</div>
)
}
// BarToggle: Pill-Button mit Label (+ optionalem Icon) und Active-State.
// Eignet sich fuer Toggles wie Welt/CPlane, Z-Selektor, Preset-Buttons.
export function BarToggle({ icon, label, active, onClick, title, disabled, minWidth }) {
return (
<button onClick={onClick} disabled={disabled} title={title}
onMouseEnter={(e) => {
if (disabled || active) return
e.currentTarget.style.borderColor = 'var(--accent)'
e.currentTarget.style.background = 'var(--bg-item-hover)'
}}
onMouseLeave={(e) => {
if (active) return
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.background = 'var(--bg-input)'
}}
style={{
height: BAR_H, minWidth: minWidth || BAR_H,
padding: label ? '0 10px' : 0,
background: active ? 'var(--accent)' : 'var(--bg-input)',
color: active ? 'var(--bg-panel)' : 'var(--text-primary)',
border: '1px solid ' + (active ? 'var(--accent)' : 'var(--border)'),
borderRadius: 999,
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
gap: 4, fontSize: 11,
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
appearance: 'none', WebkitAppearance: 'none',
lineHeight: 1, boxSizing: 'border-box', flexShrink: 0,
transition: 'background 0.15s, border-color 0.15s, color 0.15s',
}}>
{icon && <Icon name={icon} size={12}
style={{ color: active ? 'var(--bg-panel)' : 'var(--text-muted)' }} />}
{label && <span>{label}</span>}
</button>
)
}
// BarButton: pill-foermiger Icon-Button im selben Stil wie BarCombo.
// joinedLeft = linke Kante flach (dockt rechts an einen BarCombo).
export function BarButton({ icon, onClick, title, disabled, active, joinedLeft }) {
return (
<button onClick={onClick} disabled={disabled} title={title}
onMouseEnter={(e) => {
if (disabled || active) return
e.currentTarget.style.borderColor = 'var(--accent)'
e.currentTarget.style.background = 'var(--bg-item-hover)'
}}
onMouseLeave={(e) => {
if (active) return
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.background = 'var(--bg-input)'
}}
style={{
height: BAR_H, width: BAR_H,
background: active ? 'var(--accent)' : 'var(--bg-input)',
border: '1px solid var(--border)',
borderTopLeftRadius: joinedLeft ? 0 : 999,
borderBottomLeftRadius: joinedLeft ? 0 : 999,
borderTopRightRadius: 999, borderBottomRightRadius: 999,
borderLeft: joinedLeft ? 'none' : '1px solid var(--border)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1, flexShrink: 0,
padding: 0,
transition: 'border-color 0.15s, background 0.15s',
}}>
<Icon name={icon} size={13}
style={{ color: active ? 'var(--bg-panel)' : 'var(--text-muted)' }} />
</button>
)
}
+36 -32
View File
@@ -2,6 +2,7 @@ import { useState, useRef, useMemo, useEffect } from 'react'
import Icon from './Icon'
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
import ContextMenu from './ContextMenu'
import { BarCombo, BarButton } from './BarControls'
import { setLayerStyle, deleteEbene, moveSelectionToEbene, openEbenenSettings } from '../lib/rhinoBridge'
const MODES = [
@@ -79,11 +80,11 @@ function LwCell({ lw, onChange }) {
>{lw.toFixed(2)}</span>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
<button className="btn-step" onClick={(ev) => { ev.stopPropagation(); step(+1) }} style={{ width: 10, height: 7 }}>
<Icon name="arrow_drop_up" size={10} />
<button className="btn-step" onClick={(ev) => { ev.stopPropagation(); step(+1) }} style={{ width: 10, height: 6 }}>
<Icon name="arrow_drop_up" size={9} />
</button>
<button className="btn-step" onClick={(ev) => { ev.stopPropagation(); step(-1) }} style={{ width: 10, height: 7 }}>
<Icon name="arrow_drop_down" size={10} />
<button className="btn-step" onClick={(ev) => { ev.stopPropagation(); step(-1) }} style={{ width: 10, height: 6 }}>
<Icon name="arrow_drop_down" size={9} />
</button>
</div>
</div>
@@ -243,21 +244,21 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
onClick={onClick}
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '3px 12px',
paddingLeft: 12 + (depth || 0) * 14,
margin: active ? '1px 6px' : '0',
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 12px',
paddingLeft: 12 + (depth || 0) * 12,
margin: 0,
background: active ? 'var(--active-dim)'
: (e.visible !== false) ? 'var(--bg-item)'
: 'var(--bg-panel)',
// Pill-Form fuer die aktive Ebene, sonst Standard-Zeile mit Bottom-Border
borderRadius: active ? 999 : 0,
borderLeft: active ? 'none' : '3px solid transparent',
borderBottom: active ? 'none' : '1px solid var(--border-light)',
boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none',
// Eckige Tabellen-Zeile mit Accent-Strip links fuer aktive Ebene
borderRadius: 0,
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
borderBottom: '1px solid var(--border-light)',
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
cursor: 'pointer',
userSelect: 'none',
minHeight: 24,
}}
>
{hasChildren ? (
@@ -265,24 +266,24 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
title={expanded ? 'Einklappen' : 'Aufklappen'}
style={{ width: 14, height: 14 }}
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={12} /></button>
style={{ width: 12, height: 12 }}
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button>
) : (
<span style={{ width: 14, flexShrink: 0 }} />
<span style={{ width: 12, flexShrink: 0 }} />
)}
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
title={eyeTitle}
style={{ opacity: eyeOpacity }}
><Icon name={eyeIcon} size={14} /></button>
style={{ opacity: eyeOpacity, width: 16, height: 16 }}
><Icon name={eyeIcon} size={12} /></button>
<EditableText
value={e.code}
onCommit={onCodeChange}
autoEditTrigger={autoEditCode}
fontSize={9}
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', width: 24, textAlign: 'left', flexShrink: 0 }}
style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-muted)', width: 22, textAlign: 'left', flexShrink: 0 }}
/>
<ColorPicker color={e.color} onChange={onColorChange} />
@@ -300,6 +301,7 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
: 'var(--text-muted)',
display: 'inline-block', width: '100%',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
lineHeight: 1.2,
}}
/>
</div>
@@ -310,14 +312,15 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
title={e.locked ? 'Entsperren' : 'Sperren'}
style={{ color: e.locked ? 'var(--warn)' : undefined }}
><Icon name={e.locked ? 'lock' : 'lock_open'} size={12} /></button>
style={{ color: e.locked ? 'var(--warn)' : undefined, width: 14, height: 14 }}
><Icon name={e.locked ? 'lock' : 'lock_open'} size={11} /></button>
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
title="Löschen"
><Icon name="close" size={12} /></button>
style={{ width: 14, height: 14 }}
><Icon name="close" size={11} /></button>
</div>
)
}
@@ -547,16 +550,17 @@ export default function EbenenManager({
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<select
value={mode}
onChange={ev => onModeChange(ev.target.value)}
style={{ flex: 1, minWidth: 0 }}
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</select>
<button className="btn-icon-sm" onClick={addNew} title="Ebene hinzufügen">
<Icon name="add" size={14} />
</button>
<div style={{ flex: 1, minWidth: 0, display: 'flex' }}>
<BarCombo
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
>
{MODES.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
</BarCombo>
</div>
<BarButton icon="add" onClick={addNew} title="Ebene hinzufügen" />
</div>
</div>
+33 -31
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'
import Icon from './Icon'
import ContextMenu from './ContextMenu'
import { BarCombo, BarButton } from './BarControls'
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
function GeschossBadge({ name }) {
@@ -46,31 +47,32 @@ function ZeichnungsebeneRow({
onClick={onClick}
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '4px 12px',
margin: active ? '1px 6px' : '0',
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 12px',
margin: 0,
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
borderRadius: active ? 999 : 0,
borderLeft: active ? 'none' : '3px solid transparent',
borderBottom: active ? 'none' : '1px solid var(--border-light)',
boxShadow: active ? 'inset 0 0 0 1px var(--active-light)' : 'none',
borderRadius: 0,
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
borderBottom: '1px solid var(--border-light)',
cursor: 'pointer',
userSelect: 'none',
minHeight: 24,
}}
>
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
onClick={(ev) => { ev.stopPropagation(); onToggleVisible() }}
title={eyeTitle}
style={{ opacity: eyeOpacity }}
><Icon name={eyeIcon} size={14} /></button>
style={{ opacity: eyeOpacity, width: 16, height: 16 }}
><Icon name={eyeIcon} size={12} /></button>
<span style={{
fontWeight: active ? 700 : 500,
fontSize: 12,
fontSize: 11,
color: active ? 'var(--active-light)' : 'var(--text-label)',
flex: 1, minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
lineHeight: 1.2,
}}>{z.name}</span>
{isGeschoss && (
@@ -88,24 +90,25 @@ function ZeichnungsebeneRow({
title={z.hasClipping
? 'Clipping Plane ausschalten'
: 'Clipping Plane einschalten (Schnitt auf Schnitthöhe)'}
style={{ color: z.hasClipping ? 'var(--accent)' : undefined }}
><Icon name="content_cut" size={12} /></button>
style={{ color: z.hasClipping ? 'var(--accent)' : undefined, width: 14, height: 14 }}
><Icon name="content_cut" size={11} /></button>
) : (
<span style={{ width: 18, flexShrink: 0 }} />
<span style={{ width: 14, flexShrink: 0 }} />
)}
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleLock() }}
title={z.locked ? 'Entsperren' : 'Sperren'}
style={{ color: z.locked ? 'var(--warn)' : undefined }}
><Icon name={z.locked ? 'lock' : 'lock_open'} size={12} /></button>
style={{ color: z.locked ? 'var(--warn)' : undefined, width: 14, height: 14 }}
><Icon name={z.locked ? 'lock' : 'lock_open'} size={11} /></button>
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onDelete() }}
title="Löschen"
><Icon name="close" size={12} /></button>
style={{ width: 14, height: 14 }}
><Icon name="close" size={11} /></button>
</div>
)
}
@@ -215,21 +218,20 @@ export default function GeschossManager({
}}>
<span className="label-xs">Sichtbarkeit</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<select
value={mode}
onChange={ev => onModeChange(ev.target.value)}
style={{ flex: 1, minWidth: 0 }}
>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
<button className="btn-icon-sm" onClick={addQuick} title="Zeichnungsebene hinzufügen">
<Icon name="add" size={14} />
</button>
<button className="btn-icon-sm" onClick={() => openGeschossDialog(zeichnungsebenen)} title="Bearbeiten">
<Icon name="edit" size={13} />
</button>
<div style={{ flex: 1, minWidth: 0, display: 'flex' }}>
<BarCombo
icon="visibility"
value={mode}
onChange={onModeChange}
title="Sichtbarkeits-Modus"
>
{MODES.map(m => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</BarCombo>
</div>
<BarButton icon="add" onClick={addQuick} title="Zeichnungsebene hinzufügen" />
<BarButton icon="settings" onClick={() => openGeschossDialog(zeichnungsebenen)} title="Einstellungen" />
</div>
</div>