import { useState, useEffect, useMemo } from 'react' import Icon from './components/Icon' import ContextMenu from './components/ContextMenu' import { onMessage, notifyReady, listAusschnitte, saveAusschnitt, updateAusschnitt, restoreAusschnitt, applyAusschnittToDetail, renameAusschnitt, deleteAusschnitt, setAusschnittFolder, setAusschnittScale, duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder, openAusschnittSettings, } from './lib/rhinoBridge' function EditableInline({ value, onCommit, autoEdit, style, fontSize }) { const [editing, setEditing] = useState(autoEdit || false) const [val, setVal] = useState(value) useEffect(() => { setVal(value) }, [value]) useEffect(() => { if (autoEdit) setEditing(true) }, [autoEdit]) const commit = () => { const trimmed = (val ?? '').trim() if (trimmed && trimmed !== value) onCommit(trimmed) else setVal(value) setEditing(false) } if (editing) { return ( setVal(ev.target.value)} onBlur={commit} onKeyDown={(ev) => { if (ev.key === 'Enter') commit() if (ev.key === 'Escape') { setVal(value); setEditing(false) } }} onClick={(ev) => ev.stopPropagation()} style={{ ...style, fontSize, padding: '2px 6px' }} /> ) } return ( { ev.stopPropagation(); setVal(value); setEditing(true) }} style={{ ...style, fontSize, cursor: 'text' }} >{value} ) } function ScaleCell({ snap, onChange }) { const [editing, setEditing] = useState(false) const [val, setVal] = useState(snap.scale || '') useEffect(() => { setVal(snap.scale || '') }, [snap.scale]) const commit = () => { onChange(snap.id, val.trim()) setEditing(false) } if (editing) { return ( setVal(ev.target.value)} onBlur={commit} onKeyDown={(ev) => { if (ev.key === 'Enter') commit() if (ev.key === 'Escape') { setVal(snap.scale || ''); setEditing(false) } }} onClick={(ev) => ev.stopPropagation()} placeholder="1:50" style={{ width: 64, fontSize: 10, padding: '2px 6px', textAlign: 'right' }} /> ) } return ( { ev.stopPropagation(); setEditing(true) }} className={snap.scale ? 'chip chip-accent' : 'chip'} style={{ fontSize: 9, cursor: 'text', fontFamily: 'var(--font-mono)', flexShrink: 0, }} title={snap.scale ? `Maßstab ${snap.scale} — wird auf Detail angewendet` : 'Doppelklick um Maßstab einzutragen (z.B. 1:50)'} >{snap.scale || '—:—'} ) } function OrientationBadge({ orientation }) { const variant = orientation === 'perspective' ? { icon: 'view_in_ar', color: 'var(--accent)', title: 'Perspektive', } : orientation === 'horizontal' ? { icon: 'align_horizontal_center', color: 'var(--active)', title: 'Horizontaler Schnitt (Grundriss)', } : { icon: 'align_vertical_center', color: 'var(--warn)', title: 'Vertikaler Schnitt (Schnitt / Ansicht)', } return ( ) } function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, onScaleChange, onDragStart, onDragEnd, dragging }) { return (
{ ev.currentTarget.style.background = 'var(--bg-item-hover)' }} onMouseLeave={(ev) => { ev.currentTarget.style.background = 'var(--bg-input)' }} > onRename(snap.id, n)} fontSize={11} style={{ flex: 1, minWidth: 0, color: 'var(--text-primary)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }} />
) } function FolderCard({ name, count, collapsed, onToggle, onContextMenu, onMenuClick, onDragOver, onDragLeave, onDrop, dragOver, children, }) { return (
{name} {count}
{!collapsed && (
{children || (
Leer — Ausschnitte hier ablegen.
)}
)}
) } function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, empty }) { return (
{children}
) } export default function AusschnitteApp() { const [snaps, setSnaps] = useState([]) const [extraFolders, setExtraFolders] = useState([]) const [newName, setNewName] = useState('') const [ctxMenu, setCtxMenu] = useState(null) const [collapsed, setCollapsed] = useState({}) const [draggingId, setDraggingId] = useState(null) const [dragTarget, setDragTarget] = useState(null) useEffect(() => { onMessage('LIST', ({ snapshots, folders }) => { setSnaps(snapshots || []) setExtraFolders(folders || []) }) notifyReady() const blockContext = (ev) => ev.preventDefault() document.addEventListener('contextmenu', blockContext) return () => document.removeEventListener('contextmenu', blockContext) }, []) const groups = useMemo(() => { const map = {} snaps.forEach(s => { const f = s.folder || '' if (!map[f]) map[f] = [] map[f].push(s) }) return map }, [snaps]) const allFolders = useMemo(() => { const set = new Set(extraFolders) snaps.forEach(s => { if (s.folder) set.add(s.folder) }) return [...set].sort((a, b) => a.localeCompare(b)) }, [snaps, extraFolders]) const handleSave = () => { const name = newName.trim() || `Ausschnitt ${snaps.length + 1}` saveAusschnitt(name) setNewName('') } const handleAddFolder = () => { const name = window.prompt('Name für neuen Ordner:') if (name && name.trim()) addAusschnittFolder(name.trim()) } const ctxItems = (id) => [ { label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) }, { label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) }, { divider: true }, { label: 'Ausschnittseinstellungen…', icon: 'tune', onClick: () => openAusschnittSettings(id) }, { divider: true }, { label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) }, { label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) }, { divider: true }, { label: 'Löschen', icon: 'delete', danger: true, onClick: () => deleteAusschnitt(id) }, ] const folderCtxItems = (folderName) => [ { label: 'Ordner umbenennen', icon: 'edit', onClick: () => { const newName = window.prompt('Neuer Ordnername:', folderName) if (newName && newName.trim() && newName !== folderName) { snaps.filter(s => s.folder === folderName).forEach(s => setAusschnittFolder(s.id, newName.trim())) addAusschnittFolder(newName.trim()) removeAusschnittFolder(folderName) } }}, { divider: true }, { label: 'Ordner löschen', icon: 'folder_off', danger: true, onClick: () => { if (window.confirm(`Ordner "${folderName}" löschen? Ausschnitte werden zur Wurzel verschoben.`)) { removeAusschnittFolder(folderName) } }}, ] const handleDrop = (folderName) => (ev) => { ev.preventDefault() setDragTarget(null) const id = ev.dataTransfer.getData('text/plain') || draggingId if (id) setAusschnittFolder(id, folderName || '') setDraggingId(null) } const handleDragOver = (folderName) => (ev) => { ev.preventDefault() ev.dataTransfer.dropEffect = 'move' setDragTarget(folderName || 'root') } const handleDragLeave = () => () => setDragTarget(null) const renderSnapshot = (s) => ( restoreAusschnitt(s.id)} onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' }) }} onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' })} onRename={(id, name) => renameAusschnitt(id, name)} onScaleChange={(id, scale) => setAusschnittScale(id, scale)} onDragStart={(ev) => { ev.dataTransfer.setData('text/plain', s.id) ev.dataTransfer.effectAllowed = 'move' setDraggingId(s.id) }} onDragEnd={() => { setDraggingId(null); setDragTarget(null) }} /> ) const rootItems = groups[''] || [] const isEmpty = snaps.length === 0 && allFolders.length === 0 return (
{/* Save-Bar als Card */}
setNewName(ev.target.value)} onKeyDown={(ev) => { if (ev.key === 'Enter') handleSave() }} placeholder="Name für neuen Ausschnitt…" style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font)', minWidth: 0 }} />
{isEmpty ? (
Noch keine Ausschnitte.
Oben einen Namen eingeben und klicken.
) : ( <> {/* Root-Snapshots */} {rootItems.map(s => renderSnapshot(s))} {rootItems.length === 0 && draggingId && (
Hier ablegen für Wurzel
)}
{/* Ordner-Cards */} {allFolders.map(folder => { const isCollapsed = !!collapsed[folder] const items = groups[folder] || [] return ( setCollapsed(c => ({ ...c, [folder]: !c[folder] }))} onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' }) }} onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' })} onDragOver={handleDragOver(folder)} onDragLeave={handleDragLeave()} onDrop={handleDrop(folder)} > {items.length > 0 ? items.map(s => renderSnapshot(s)) : null} ) })} )}
Drag & Drop auf Ordner-Card zum Verschieben · Doppelklick auf Name/Maßstab = bearbeiten · ⋮ für Aktionen
{/* Sticky Footer: Anzahl + Ordner erstellen + Reload */}
{snaps.length} Ausschnitte
{ctxMenu && ( setCtxMenu(null)} /> )}
) }