Satelliten-Dialoge: embedded-Mode + GeschossDialog auch als echtes Fenster

Zwei Dinge:

1. embedded-Mode in den Dialog-Komponenten — wenn TRUE, kein Backdrop +
   keine MaxWidth-Constraint, das Dialog fuellt das ganze WebView statt
   wie ein kleines zentriertes Fenster IN dem WebView gerendert zu werden
   (= "Fenster im Fenster"-Effekt). Betroffen:
   - GeschossSettingsDialog
   - EbenenSettingsDialog
   - GeschossDialog
   Satelliten-Apps (GeschossSettingsApp, EbenenSettingsApp,
   GeschossDialogApp) passen jetzt `embedded` durch.

2. GeschossDialog (= der grosse Mehrfach-Editor hinter dem Pencil-Button)
   laeuft jetzt auch als Satelliten-Fenster — selbe Architektur wie die
   Settings-Dialoge. Backend hat neuen Handler _open_geschoss_dialog und
   neuen Message OPEN_GESCHOSS_DIALOG. Auf Save: ganze z-Liste replace
   + _apply(save_z=True).

GeschossManager braucht den inline-Dialog-State nicht mehr; Pencil-Button
ruft openGeschossDialog(zeichnungsebenen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 01:30:28 +02:00
parent 1ba0bda429
commit b1b2090b3e
10 changed files with 184 additions and 94 deletions
+7 -12
View File
@@ -26,17 +26,12 @@ export default function EbenenSettingsApp() {
}
return (
<div style={{
width: '100vw', height: '100vh',
background: 'var(--bg-base)',
overflow: 'hidden',
}}>
<EbenenSettingsDialog
ebene={ebene}
hatchPatterns={hatchPatterns}
onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})}
/>
</div>
<EbenenSettingsDialog
embedded
ebene={ebene}
hatchPatterns={hatchPatterns}
onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})}
/>
)
}
+49
View File
@@ -0,0 +1,49 @@
import { useEffect } from 'react'
import GeschossDialog from './components/GeschossDialog'
import { notifyReady } from './lib/rhinoBridge'
function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload })
document.title = 'RHINOMSG::' + json
}
// recalcOkff direkt hier — gleiche Logik wie in ZeichnungsebenenApp.jsx,
// damit der Dialog die OKFF-Werte beim Editieren live updaten kann.
function recalcOkff(list) {
let acc = 0
return list.map(z => {
if (z.isGeschoss) {
const next = { ...z, okff: parseFloat(acc.toFixed(3)) }
acc += (z.hoehe ?? 3.0)
return next
}
return { ...z, okff: undefined }
})
}
export default function GeschossDialogApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
useEffect(() => {
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const z = initial.zeichnungsebenen || initial
if (!Array.isArray(z) || z.length === 0) {
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
}
return (
<GeschossDialog
embedded
zeichnungsebenen={z}
recalcOkff={recalcOkff}
onSave={(updated) => bridgeSend('SAVE', { zeichnungsebenen: updated })}
onClose={() => bridgeSend('CANCEL', {})}
/>
)
}
+7 -17
View File
@@ -1,9 +1,7 @@
import { useEffect } from 'react'
import GeschossSettingsDialog from './components/GeschossSettingsDialog'
import { onMessage, notifyReady } from './lib/rhinoBridge'
import { notifyReady } from './lib/rhinoBridge'
// Inline send fuer SAVE/CANCEL — schickt direkt an Python-Bridge des
// Satelliten-Fensters.
function bridgeSend(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
const json = JSON.stringify({ type, payload })
@@ -11,33 +9,25 @@ function bridgeSend(type, payload = {}) {
}
export default function GeschossSettingsApp() {
// PANEL_PARAMS wurden von Python beim Window-Open injiziert.
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
useEffect(() => {
notifyReady()
// Native Browser-Context-Menu unterdruecken
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
// Wenn keine Daten da sind: leeres Frame
if (!initial || typeof initial !== 'object') {
return <div style={{ padding: 20, color: 'var(--text-muted)' }}>Keine Daten</div>
}
return (
<div style={{
width: '100vw', height: '100vh',
background: 'var(--bg-base)',
overflow: 'hidden',
}}>
<GeschossSettingsDialog
geschoss={initial}
onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})}
/>
</div>
<GeschossSettingsDialog
embedded
geschoss={initial}
onSave={(updated) => bridgeSend('SAVE', updated)}
onClose={() => bridgeSend('CANCEL', {})}
/>
)
}
+29 -17
View File
@@ -27,7 +27,7 @@ function SectionLabel({ children }) {
)
}
export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose }) {
export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'], onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState({
...ebene,
fill: {
@@ -78,23 +78,35 @@ export default function EbenenSettingsDialog({ ebene, hatchPatterns = ['Solid'],
...hatchPatterns.filter(p => p !== 'Solid' && p !== 'None'),
]
const wrapperStyle = embedded ? {
width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30,
}
const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 16px)', maxWidth: 300,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
maxHeight: 'calc(100vh - 60px)',
}
return (
<div style={{
position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30,
}}>
<div style={{
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 16px)', maxWidth: 300,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
maxHeight: 'calc(100vh - 60px)',
}}>
<div style={wrapperStyle}>
<div style={innerStyle}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
+28 -17
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'
import Icon from './Icon'
export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose }) {
export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z })))
const update = (i, field, val) => {
@@ -56,23 +56,34 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
del: { width: 22, flexShrink: 0 },
}
const wrapperStyle = embedded ? {
width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
position: 'absolute', inset: 0, zIndex: 100,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 40,
}
const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
width: 'calc(100% - 24px)',
boxShadow: 'var(--shadow)',
display: 'flex', flexDirection: 'column',
maxHeight: 'calc(100vh - 80px)',
overflow: 'hidden',
}
return (
<div style={{
position: 'absolute', inset: 0, zIndex: 100,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 40,
}}>
<div style={{
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
width: 'calc(100% - 24px)',
boxShadow: 'var(--shadow)',
display: 'flex', flexDirection: 'column',
maxHeight: 'calc(100vh - 80px)',
overflow: 'hidden',
}}>
<div style={wrapperStyle}>
<div style={innerStyle}>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
+4 -14
View File
@@ -1,7 +1,5 @@
import { useState } from 'react'
import Icon from './Icon'
import GeschossDialog from './GeschossDialog'
import { openGeschossSettings } from '../lib/rhinoBridge'
import { openGeschossSettings, openGeschossDialog } from '../lib/rhinoBridge'
function GeschossBadge({ name }) {
return <span className="chip chip-info">{name}</span>
@@ -85,7 +83,8 @@ export default function GeschossManager({
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
mode, onModeChange,
}) {
const [dialogOpen, setDialogOpen] = useState(false)
// dialogOpen-State entfaellt — Bearbeiten-Dialog laeuft jetzt als
// Satelliten-Fenster via openGeschossDialog().
const sorted = [...zeichnungsebenen].reverse()
const gesamthoehe = zeichnungsebenen
@@ -134,7 +133,7 @@ export default function GeschossManager({
<button className="btn-icon-sm" onClick={addQuick} title="Zeichnungsebene hinzufügen">
<Icon name="add" size={14} />
</button>
<button className="btn-icon-sm" onClick={() => setDialogOpen(true)} title="Bearbeiten">
<button className="btn-icon-sm" onClick={() => openGeschossDialog(zeichnungsebenen)} title="Bearbeiten">
<Icon name="edit" size={13} />
</button>
</div>
@@ -166,15 +165,6 @@ export default function GeschossManager({
))}
</div>
{dialogOpen && (
<GeschossDialog
zeichnungsebenen={zeichnungsebenen}
recalcOkff={recalcOkff}
onSave={(updated) => { onChange(updated); setDialogOpen(false) }}
onClose={() => setDialogOpen(false)}
/>
)}
</>
)
}
+32 -17
View File
@@ -35,7 +35,7 @@ function Toggle({ label, checked, onChange, hint }) {
)
}
export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embedded = false }) {
const [draft, setDraft] = useState({ ...geschoss })
const set = (patch) => setDraft({ ...draft, ...patch })
@@ -46,22 +46,37 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
const okff = draft.okff ?? 0
const clipZ = (okff + schnitt).toFixed(2)
// embedded=true: in einem Satelliten-Fenster gerendert — kein Backdrop,
// keine Width-Constraint, fuellt das ganze WebView.
const Wrapper = embedded ? 'div' : 'div'
const wrapperStyle = embedded ? {
width: '100%', height: '100%',
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30,
}
const innerStyle = embedded ? {
width: '100%', height: '100%',
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
} : {
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 16px)', maxWidth: 280,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
}
return (
<div style={{
position: 'absolute', inset: 0, zIndex: 150,
background: 'var(--bg-overlay)',
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
paddingTop: 30,
}}>
<div style={{
background: 'var(--bg-dialog)',
border: '1px solid var(--border)',
borderRadius: 'var(--r-lg)',
boxShadow: 'var(--shadow-3)',
width: 'calc(100% - 16px)', maxWidth: 280,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
}}>
<Wrapper style={wrapperStyle}>
<div style={innerStyle}>
{/* Header */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
@@ -155,6 +170,6 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose }) {
}}>Übernehmen</button>
</div>
</div>
</div>
</Wrapper>
)
}
+3
View File
@@ -285,6 +285,9 @@ export function openGeschossSettings(geschoss) {
export function openEbenenSettings(ebene, hatchPatterns) {
send('OPEN_EBENEN_SETTINGS', { ebene, hatchPatterns })
}
export function openGeschossDialog(zeichnungsebenen) {
send('OPEN_GESCHOSS_DIALOG', { zeichnungsebenen })
}
export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) {
// Split-Panels koennen mit null/[] fuer fremde Slice aufrufen — Backend
+2
View File
@@ -5,6 +5,7 @@ import App from './App.jsx'
import ZeichnungsebenenApp from './ZeichnungsebenenApp.jsx'
import GeschossSettingsApp from './GeschossSettingsApp.jsx'
import EbenenSettingsApp from './EbenenSettingsApp.jsx'
import GeschossDialogApp from './GeschossDialogApp.jsx'
import GestaltungApp from './GestaltungApp.jsx'
import AusschnitteApp from './AusschnitteApp.jsx'
import MassstabApp from './MassstabApp.jsx'
@@ -28,6 +29,7 @@ const RootApp = mode === 'gestaltung' ? GestaltungApp
: mode === 'zeichnungsebenen' ? ZeichnungsebenenApp
: mode === 'geschoss_settings' ? GeschossSettingsApp
: mode === 'ebenen_settings' ? EbenenSettingsApp
: mode === 'geschoss_dialog' ? GeschossDialogApp
: App
window.onerror = function (msg, src, line, col, err) {