13a5e1eb7a
Lizenz: - AGPL-3.0 LICENSE-File im Repo-Root (GNU Volltext) - SPDX-Header + Copyright in allen Source-Files (Python/JSX/JS/Rust) - license-Feld in package.json + Cargo.toml - About-App komplett neu: Dual-Lizenz-Block (AGPL + Commercial), openbureau-Branding, Version-Pills, made-in-Switzerland-Footer UI-Restyle (3 Wellen) — alle Dialoge + Satellites + Panel-Sidebars auf gemeinsamen Pill-Stil aus BarControls (BarToggle/BarButton/BarCombo): - Welle 1: GeschossDialog/Settings, AusschnittSettings, LayoutDialog - Welle 2: ConfirmDeleteEbene, Kamera, MasseSettings, Osm, Swisstopo, TextEditor, AusschnittLayerDialog, LayerCombinations - Welle 3: LayoutsApp, MassstabApp, WerkzeugeApp, OverridesApp, ZeichnungsebenenApp; Werkzeuge mit ElementeApp-PillGroup-Layout GeschossDialog Header-Refactor: +Geschoss/+Zeichnung in Toolbar oben, move-Pfeile-Spalte breiter (kein Overlap mit G-Haken) Ausschnitte Rows als Pills, kein Outer-Border ums Suchfeld Section-Style komplett neu (gestaltung.py + GestaltungApp.jsx): - ObjectSectionAttributesSource.FromObject (richtiger Enum-Name fuer Mac) - HatchPatternPrintColor + BoundaryPrintColor mit-setzen (Display = Print) - BoundaryColor nur bei explizitem User-Override, sonst Rhino-Default - background_color_hex Parameter (BackgroundFillMode=SolidColor) - Readback aus GetCustomSectionStyle statt direkt aus Attributes - UI: Schnittkante > Section Style > Solid-Fill mit proper SectionHead - 'Boundary' (3D Pen) -> 'Background' weil sich's wie Section-Hintergrund verhaelt Plan-Mode 'Dossier Plan' via Template: - rhino/templates/dossier_plan.ini wird direkt geladen - Fallback auf Technical-Clone + ini-Patch wenn Template fehlt - Auto-Cleanup von Orphan-Modes vor Import (Name- oder Guid-Match) - ClipSectionUsage=1 + TechnicalMask=15 als bekannte Soll-Werte - Bei Template-Pfad keine ini-Patches (1:1 wie User exportiert) - Sanity-Print listet alle registrierten Modes nach Anlegen Bridge-Unification: 4 Settings-Apps (Ebenen/Project/Geschoss*Dialog) benutzen jetzt chunkende send() statt eigene bridgeSend ohne Chunk- Logik -> grosse Payloads (Hatch-Refs etc.) kommen nicht mehr truncated bei Python an (loeste 'JSON-Fehler char 990'-Regression in Ebenen- Settings) Library-Imports robust: 'import library' jetzt Top-Level in elemente.py + rhinopanel.py (statt Lazy in Methoden) -> 'No module named library'- Crashes weg auch wenn sys.path zwischendurch resettet wird Tools fuer Display-Mode-Maintenance: - _clean_display_modes.py (loescht alle Custom-Modes, Built-ins bleiben) - _inspect_plan_mode.py / _inspect_obj_section.py / _inspect_obj_boundary.py (Diagnose-Skripte fuer SectionStyle-Property-Reverse-Engineering) - _reset_rhino_settings.sh (Backup + Nuke der Rhino-Settings als letzte Bastion gegen korrupte Display-Modes)
210 lines
7.2 KiB
React
210 lines
7.2 KiB
React
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Copyright (C) 2026 Karim Gabriele Varano
|
|
import { useState, useMemo } from 'react'
|
|
import Icon from './Icon'
|
|
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
|
|
|
/* Preview — abhaengig vom Typ:
|
|
material → Color-Swatch
|
|
symbol/object → Typ-Icon-Placeholder (spaeter: PNG-Thumbnail aus
|
|
library/previews/) */
|
|
function ItemPreview({ item }) {
|
|
if (item.type === 'material') {
|
|
return (
|
|
<div style={{
|
|
height: 64,
|
|
background: item.data?.color || '#888888',
|
|
borderBottom: '1px solid var(--border-light)',
|
|
}} />
|
|
)
|
|
}
|
|
const iconName = item.type === 'symbol' ? 'navigation' : 'forest'
|
|
return (
|
|
<div style={{
|
|
height: 64,
|
|
background: 'var(--bg-input)',
|
|
borderBottom: '1px solid var(--border-light)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
<Icon name={iconName} size={28}
|
|
style={{ color: 'var(--text-muted)' }} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ItemCard — Preview + Name + Tags + Import-Pill */
|
|
function ItemCard({ item, imported, onImport }) {
|
|
// material wird "importiert" gezeigt wenn schon in Project-Settings.
|
|
// symbol/object koennen N-mal eingefuegt werden → "Importiert" wird
|
|
// nicht persistent, jeder Klick legt eine weitere Instanz an.
|
|
const isMaterial = item.type === 'material'
|
|
const ctaLabel = isMaterial
|
|
? (imported ? 'Importiert' : 'Importieren')
|
|
: 'Einfügen'
|
|
const ctaDisabled = isMaterial && imported
|
|
return (
|
|
<div style={{
|
|
display: 'flex', flexDirection: 'column',
|
|
border: '1px solid var(--border)', borderRadius: 8,
|
|
background: 'var(--bg-section)',
|
|
overflow: 'hidden',
|
|
cursor: ctaDisabled ? 'default' : 'pointer',
|
|
opacity: ctaDisabled ? 0.75 : 1,
|
|
transition: 'border-color 0.15s, background 0.15s',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (ctaDisabled) return
|
|
e.currentTarget.style.borderColor = 'var(--accent)'
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.borderColor = 'var(--border)'
|
|
}}
|
|
onDoubleClick={() => { if (!ctaDisabled) onImport(item.id) }}
|
|
title={ctaDisabled ? 'Bereits importiert' : 'Doppelklick = importieren'}>
|
|
<ItemPreview item={item} />
|
|
<div style={{ padding: '8px 10px', display: 'flex',
|
|
flexDirection: 'column', gap: 6 }}>
|
|
<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>
|
|
{ctaDisabled && (
|
|
<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 6px', borderRadius: 999,
|
|
border: '1px solid var(--border-light)',
|
|
}}>{t}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
<BarToggle
|
|
label={ctaLabel}
|
|
active={!ctaDisabled}
|
|
onClick={() => { if (!ctaDisabled) onImport(item.id) }}
|
|
disabled={ctaDisabled} />
|
|
</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,
|
|
}}>
|
|
{/* Toolbar — Suche + Pill-Filter + Reload */}
|
|
<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, height: BAR_H, padding: '0 12px',
|
|
fontSize: 11 }} />
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
{types.map(t => (
|
|
<BarToggle key={t}
|
|
label={t === 'all' ? 'Alle' : t}
|
|
active={typeFilter === t}
|
|
onClick={() => setTypeFilter(t)} />
|
|
))}
|
|
</div>
|
|
<BarButton icon="refresh" onClick={onReload}
|
|
title="Manifest neu laden" />
|
|
</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 => (
|
|
<ItemCard 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>
|
|
)
|
|
}
|