Projekt-Settings-Dialog + Library Phase A + Material-Merger
- Project-Settings-Dialog (Voreinstellungen Geschoss/Schnitt + Material-Editor) ueber Zahnrad-Icon in Oberleiste; Defaults werden in schnitte.pick_schnitt + GeschossManager als Vorgabe genommen, pro-Element-Werte unangetastet - Dossier-Library Phase A (lokal, read-only): rhino/library.py + LibraryBrowser Satellite; Seed-Manifest unter ~/Library/Application Support/Dossier/library/ - Material-Merger: _get_all_materials(doc) merged builtin _MATERIAL_LIBRARY mit Projekt-Settings-Materialien (inkl. Library-Imports); Wand-Erstellung, Sub-Layer-Anlage + Elemente-Material-Dropdown ziehen jetzt aus dem Merge Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -159,6 +159,7 @@ const MODES = [
|
||||
export default function GeschossManager({
|
||||
zeichnungsebenen, activeId, onActiveChange, onChange, recalcOkff,
|
||||
mode, onModeChange,
|
||||
projectSettings,
|
||||
}) {
|
||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
|
||||
@@ -254,16 +255,23 @@ export default function GeschossManager({
|
||||
return 'Neu'
|
||||
}
|
||||
|
||||
// Project-Default-Hoehe: erst aktives Geschoss, dann erstes Geschoss in
|
||||
// der Liste, dann hartcodiert 3.0. So uebernimmt jeder neue Eintrag den
|
||||
// "typischen" Wert des Projekts ohne dass der User irgendwo setzen muss.
|
||||
// Default-Hoehe: Hierarchie
|
||||
// 1. Project-Settings (zentrale Voreinstellung — explizit gesetzt)
|
||||
// 2. Aktives Geschoss (wahrscheinlich gleich vom User gewuenscht)
|
||||
// 3. Erstes Geschoss in der Liste
|
||||
// 4. Hartcodiert 3.0
|
||||
// Project-Settings ueberschreibt alle anderen wenn vorhanden, damit
|
||||
// der User eine zentrale Vorgabe haben kann.
|
||||
const projDefaults = projectSettings?.defaults || {}
|
||||
const defaultGeschossHoehe = () => {
|
||||
if (projDefaults.geschossHoehe != null) return projDefaults.geschossHoehe
|
||||
const act = zeichnungsebenen.find(z => z.id === activeId)
|
||||
if (act?.isGeschoss && act.hoehe != null) return act.hoehe
|
||||
const first = zeichnungsebenen.find(z => z.isGeschoss && z.hoehe != null)
|
||||
return first?.hoehe ?? 3.0
|
||||
}
|
||||
const defaultSchnitthoehe = () => {
|
||||
if (projDefaults.schnitthoehe != null) return projDefaults.schnitthoehe
|
||||
const act = zeichnungsebenen.find(z => z.id === activeId)
|
||||
if (act?.isGeschoss && act.schnitthoehe != null) return act.schnitthoehe
|
||||
const first = zeichnungsebenen.find(z => z.isGeschoss && z.schnitthoehe != null)
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import Icon from './Icon'
|
||||
|
||||
/* MaterialCard — Preview-Swatch + Name + Tags + Import-Button. */
|
||||
function MaterialCard({ item, imported, onImport }) {
|
||||
const color = item.data?.color || '#888888'
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
border: '1px solid var(--border)', borderRadius: 6,
|
||||
background: 'var(--bg-section)',
|
||||
overflow: 'hidden',
|
||||
cursor: imported ? 'default' : 'pointer',
|
||||
opacity: imported ? 0.75 : 1,
|
||||
}}
|
||||
onDoubleClick={() => { if (!imported) onImport(item.id) }}
|
||||
title={imported ? 'Bereits importiert' : 'Doppelklick = importieren'}>
|
||||
<div style={{
|
||||
height: 64,
|
||||
background: color,
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}} />
|
||||
<div style={{ padding: '6px 8px', display: 'flex',
|
||||
flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ flex: 1, fontSize: 11, fontWeight: 600,
|
||||
color: 'var(--text-primary)',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' }}>
|
||||
{item.name}
|
||||
</span>
|
||||
{imported && (
|
||||
<Icon name="check" size={12}
|
||||
style={{ color: 'var(--accent)' }} />
|
||||
)}
|
||||
</div>
|
||||
{(item.tags || []).length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 3 }}>
|
||||
{(item.tags || []).slice(0, 4).map(t => (
|
||||
<span key={t} style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
background: 'var(--bg-input)',
|
||||
padding: '1px 5px', borderRadius: 999,
|
||||
}}>{t}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { if (!imported) onImport(item.id) }}
|
||||
disabled={imported}
|
||||
style={{
|
||||
marginTop: 2, padding: '3px 6px', fontSize: 10,
|
||||
background: imported ? 'transparent' : 'var(--accent)',
|
||||
color: imported ? 'var(--text-muted)' : '#fff',
|
||||
border: imported ? '1px solid var(--border)' : 'none',
|
||||
borderRadius: 999, cursor: imported ? 'default' : 'pointer',
|
||||
}}>
|
||||
{imported ? 'Importiert' : 'Importieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LibraryBrowser({
|
||||
manifest, importedIds, libraryRoot,
|
||||
onImport, onReload, onClose,
|
||||
}) {
|
||||
const [search, setSearch] = useState('')
|
||||
const [typeFilter, setTypeFilter] = useState('all')
|
||||
|
||||
const items = manifest?.items || []
|
||||
const importedSet = useMemo(() => new Set(importedIds || []),
|
||||
[importedIds])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase()
|
||||
return items.filter(it => {
|
||||
if (typeFilter !== 'all' && it.type !== typeFilter) return false
|
||||
if (!q) return true
|
||||
if ((it.name || '').toLowerCase().includes(q)) return true
|
||||
if ((it.tags || []).some(t => t.toLowerCase().includes(q))) return true
|
||||
return false
|
||||
})
|
||||
}, [items, search, typeFilter])
|
||||
|
||||
const types = useMemo(() => {
|
||||
const s = new Set(items.map(i => i.type))
|
||||
return ['all', ...Array.from(s)]
|
||||
}, [items])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: '100%',
|
||||
background: 'var(--bg-dialog)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="inventory_2" size={14}
|
||||
style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 11 }}>
|
||||
{manifest?.name || 'Dossier-Library'}
|
||||
</span>
|
||||
<button onClick={onReload}
|
||||
title="Manifest neu laden"
|
||||
style={{ background: 'transparent', border: 'none',
|
||||
cursor: 'pointer', color: 'var(--text-muted)',
|
||||
padding: '0 4px' }}>
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
<button onClick={onClose}
|
||||
style={{ color: 'var(--text-muted)', fontSize: 16,
|
||||
padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 12px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<input type="text" value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
placeholder="Suchen (Name oder Tag)…"
|
||||
style={{ flex: 1, fontSize: 11, padding: '4px 8px',
|
||||
borderRadius: 999 }} />
|
||||
<div style={{ display: 'flex', gap: 0,
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999, overflow: 'hidden' }}>
|
||||
{types.map(t => (
|
||||
<button key={t}
|
||||
onClick={() => setTypeFilter(t)}
|
||||
style={{
|
||||
background: typeFilter === t ? 'var(--accent)' : 'transparent',
|
||||
color: typeFilter === t ? '#fff' : 'var(--text-muted)',
|
||||
border: 'none', padding: '4px 10px', fontSize: 10,
|
||||
cursor: 'pointer',
|
||||
}}>
|
||||
{t === 'all' ? 'Alle' : t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
|
||||
padding: '10px 12px' }}>
|
||||
{filtered.length === 0 ? (
|
||||
<div style={{ padding: 24, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
{items.length === 0
|
||||
? 'Library ist leer. Manifest unter:'
|
||||
: 'Nichts gefunden — Filter aendern?'}
|
||||
{items.length === 0 && libraryRoot && (
|
||||
<div style={{ marginTop: 6, fontSize: 9, fontFamily: 'monospace' }}>
|
||||
{libraryRoot}/library.json
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
||||
gap: 8,
|
||||
}}>
|
||||
{filtered.map(it => (
|
||||
<MaterialCard key={it.id}
|
||||
item={it}
|
||||
imported={importedSet.has(it.id)}
|
||||
onImport={onImport} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '6px 12px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)',
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
}}>
|
||||
<span>{filtered.length} / {items.length} Items</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<span title={libraryRoot}
|
||||
style={{ fontFamily: 'monospace',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap', maxWidth: 320 }}>
|
||||
{libraryRoot || ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { openLibrary } from '../lib/rhinoBridge'
|
||||
|
||||
/* Inline Field + Tab helpers fuer kompaktes Layout im Satellite. */
|
||||
function Field({ label, hint, children, style }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3,
|
||||
padding: '5px 0', ...style }}>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-secondary)',
|
||||
fontWeight: 500, letterSpacing: 0.2 }}>{label}</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>{children}</div>
|
||||
{hint && (
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)', lineHeight: 1.4 }}>
|
||||
{hint}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TabBar({ tabs, active, onChange }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', gap: 0,
|
||||
borderBottom: '1px solid var(--border)',
|
||||
paddingLeft: 4,
|
||||
}}>
|
||||
{tabs.map(t => (
|
||||
<button key={t.key}
|
||||
onClick={() => onChange(t.key)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: active === t.key ? '2px solid var(--accent)' : '2px solid transparent',
|
||||
padding: '8px 14px',
|
||||
fontSize: 11, fontWeight: active === t.key ? 600 : 500,
|
||||
color: active === t.key ? 'var(--text-primary)' : 'var(--text-muted)',
|
||||
cursor: 'pointer',
|
||||
marginBottom: -1,
|
||||
}}>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MaterialRow({ mat, hatchPatterns, onChange, onDelete, builtin }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'grid', gridTemplateColumns: '14px 1fr 90px 60px 18px',
|
||||
alignItems: 'center', gap: 6,
|
||||
padding: '4px 8px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
background: builtin ? 'var(--bg-section)' : 'transparent',
|
||||
}}>
|
||||
<input type="color" value={mat.color || '#888888'}
|
||||
onChange={(ev) => onChange({ ...mat, color: ev.target.value })}
|
||||
title="Farbe"
|
||||
style={{ width: 14, height: 14, padding: 0, border: 'none',
|
||||
background: 'transparent', cursor: 'pointer' }} />
|
||||
<input type="text" value={mat.name || ''}
|
||||
onChange={(ev) => onChange({ ...mat, name: ev.target.value })}
|
||||
disabled={builtin}
|
||||
placeholder="Name"
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0,
|
||||
opacity: builtin ? 0.7 : 1 }} />
|
||||
<select value={mat.hatch || 'Solid'}
|
||||
onChange={(ev) => onChange({ ...mat, hatch: ev.target.value })}
|
||||
style={{ fontSize: 11, minWidth: 0 }}>
|
||||
{(hatchPatterns || []).map(h => (
|
||||
<option key={h} value={h}>{h}</option>
|
||||
))}
|
||||
</select>
|
||||
<input type="number" step="0.1" min="0.1"
|
||||
value={mat.scale ?? 1.0}
|
||||
onChange={(ev) => onChange({ ...mat, scale: parseFloat(ev.target.value) || 1.0 })}
|
||||
title="Hatch-Skalierung"
|
||||
style={{ fontSize: 11, textAlign: 'right' }} />
|
||||
{mat.source === 'library' ? (
|
||||
<span style={{ fontSize: 9, color: 'var(--accent-light)' }}
|
||||
title="Aus Dossier-Library">L</span>
|
||||
) : builtin || mat.source === 'builtin' ? (
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)' }}
|
||||
title="Eingebaut">B</span>
|
||||
) : (
|
||||
<button onClick={onDelete} title="Loeschen"
|
||||
style={{ background: 'transparent', border: 'none',
|
||||
padding: 0, cursor: 'pointer',
|
||||
color: 'var(--text-muted)' }}>
|
||||
<Icon name="close" size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ProjectSettingsDialog({
|
||||
initial, onSave, onClose, embedded = false,
|
||||
}) {
|
||||
const [tab, setTab] = useState('defaults')
|
||||
const [draft, setDraft] = useState(() => ({
|
||||
defaults: { ...(initial.defaults || {}) },
|
||||
materials: [...(initial.materials || [])],
|
||||
}))
|
||||
const builtin = initial.builtinMaterials || []
|
||||
const hatchPatterns = initial.hatchPatterns || ['Solid']
|
||||
|
||||
const setDefault = (k, v) =>
|
||||
setDraft(d => ({ ...d, defaults: { ...d.defaults, [k]: v } }))
|
||||
|
||||
const setMat = (i, newMat) => setDraft(d => ({
|
||||
...d, materials: d.materials.map((m, idx) => idx === i ? newMat : m),
|
||||
}))
|
||||
const delMat = (i) => setDraft(d => ({
|
||||
...d, materials: d.materials.filter((_, idx) => idx !== i),
|
||||
}))
|
||||
const addMat = () => setDraft(d => ({
|
||||
...d,
|
||||
materials: [...d.materials, {
|
||||
name: 'Neues Material', color: '#aaaaaa',
|
||||
hatch: 'Solid', scale: 1.0,
|
||||
// source/libraryId vorbereitet fuer kommendes Library-Feature:
|
||||
// 'local' = manuell erstellt, 'library' = aus Cloud-Repo, libraryId
|
||||
// referenziert dann das Library-Item per UUID.
|
||||
source: 'local', libraryId: null,
|
||||
}],
|
||||
}))
|
||||
|
||||
const wrapperStyle = embedded ? {
|
||||
width: '100%', height: '100%',
|
||||
background: 'var(--bg-dialog)',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)', fontSize: 11,
|
||||
} : {
|
||||
position: 'absolute', inset: 0, zIndex: 150,
|
||||
background: 'var(--bg-overlay)',
|
||||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
||||
paddingTop: 40,
|
||||
}
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
|
||||
minHeight: 0, overflow: 'hidden' }}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="tune" size={14}
|
||||
style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 11 }}>
|
||||
Projekt-Einstellungen
|
||||
</span>
|
||||
<button onClick={onClose}
|
||||
style={{ color: 'var(--text-muted)', fontSize: 16,
|
||||
padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||
</div>
|
||||
|
||||
<TabBar tabs={[
|
||||
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||
{ key: 'materials', label: 'Materialien' },
|
||||
]} active={tab} onChange={setTab} />
|
||||
|
||||
{/* Body */}
|
||||
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
|
||||
padding: '8px 14px' }}>
|
||||
{tab === 'defaults' && (
|
||||
<>
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
padding: '4px 0 8px', lineHeight: 1.5 }}>
|
||||
Diese Werte werden beim Erstellen neuer Elemente als
|
||||
Voreinstellung genommen. Pro-Element editierte Werte
|
||||
bleiben davon unberuehrt.
|
||||
</div>
|
||||
<Field label="STANDARD-GESCHOSSHÖHE (m)"
|
||||
hint="Vorgabe fuer neue Geschosse — pro Geschoss ueberschreibbar">
|
||||
<input type="number" step="0.05" min="1.0" max="10"
|
||||
value={draft.defaults.geschossHoehe ?? 3.0}
|
||||
onChange={(ev) => setDefault('geschossHoehe', parseFloat(ev.target.value) || 3.0)}
|
||||
style={{ flex: 1, textAlign: 'right' }} />
|
||||
</Field>
|
||||
<Field label="STANDARD-SCHNITTHÖHE (m)"
|
||||
hint="Höhe der horizontalen Schnitt-Plane über OKFF eines Geschosses">
|
||||
<input type="number" step="0.05" min="0.1" max="3"
|
||||
value={draft.defaults.schnitthoehe ?? 1.0}
|
||||
onChange={(ev) => setDefault('schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
||||
style={{ flex: 1, textAlign: 'right' }} />
|
||||
</Field>
|
||||
<div style={{ height: 1, background: 'var(--border-light)',
|
||||
margin: '8px 0' }} />
|
||||
<Field label="STANDARD-SCHNITT-TIEFE HINTEN (m)"
|
||||
hint="Default-Tiefe für neue Schnitte/Ansichten — wie weit hinter der Schnittlinie sichtbar">
|
||||
<input type="number" step="0.5" min="0.5"
|
||||
value={draft.defaults.schnittDepthBack ?? 8.0}
|
||||
onChange={(ev) => setDefault('schnittDepthBack', parseFloat(ev.target.value) || 8.0)}
|
||||
style={{ flex: 1, textAlign: 'right' }} />
|
||||
</Field>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<Field label="HÖHE UNTEN (m)" style={{ flex: 1 }}>
|
||||
<input type="number" step="0.1"
|
||||
value={draft.defaults.schnittHeightMin ?? -1.0}
|
||||
onChange={(ev) => setDefault('schnittHeightMin', parseFloat(ev.target.value))}
|
||||
style={{ flex: 1, textAlign: 'right' }} />
|
||||
</Field>
|
||||
<Field label="HÖHE OBEN (m)" style={{ flex: 1 }}>
|
||||
<input type="number" step="0.1"
|
||||
value={draft.defaults.schnittHeightMax ?? 12.0}
|
||||
onChange={(ev) => setDefault('schnittHeightMax', parseFloat(ev.target.value))}
|
||||
style={{ flex: 1, textAlign: 'right' }} />
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 'materials' && (
|
||||
<>
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
padding: '4px 0 8px', lineHeight: 1.5 }}>
|
||||
Eingebaute Materialien (B) koennen nicht umbenannt werden,
|
||||
aber Farbe + Hatch sind anpassbar. Eigene Materialien
|
||||
koennen frei angelegt werden.
|
||||
</div>
|
||||
{/* Built-in materials (read-only name) */}
|
||||
{builtin.map((m, i) => (
|
||||
<MaterialRow key={'b_' + m.name}
|
||||
mat={m}
|
||||
builtin
|
||||
hatchPatterns={hatchPatterns}
|
||||
onChange={() => {/* read-only fuer Phase 1 */}} />
|
||||
))}
|
||||
{/* User materials */}
|
||||
{draft.materials.map((m, i) => (
|
||||
<MaterialRow key={'u_' + i}
|
||||
mat={m}
|
||||
hatchPatterns={hatchPatterns}
|
||||
onChange={(nm) => setMat(i, nm)}
|
||||
onDelete={() => delMat(i)} />
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
|
||||
<button onClick={addMat}
|
||||
style={{
|
||||
padding: '4px 10px', fontSize: 11,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999, cursor: 'pointer',
|
||||
color: 'var(--text-primary)',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
}}>
|
||||
<Icon name="add" size={12} /> Material hinzufuegen
|
||||
</button>
|
||||
<button onClick={openLibrary}
|
||||
title="Material aus Dossier-Library importieren"
|
||||
style={{
|
||||
padding: '4px 10px', fontSize: 11,
|
||||
background: 'transparent',
|
||||
border: '1px solid var(--accent)',
|
||||
borderRadius: 999, cursor: 'pointer',
|
||||
color: 'var(--accent)',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
}}>
|
||||
<Icon name="inventory_2" size={12} /> Library oeffnen
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 12px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={() => onSave(draft)}>
|
||||
Übernehmen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user