diff --git a/src/ui/ResourceManager.tsx b/src/ui/ResourceManager.tsx
index 9d5d639..57a39fc 100644
--- a/src/ui/ResourceManager.tsx
+++ b/src/ui/ResourceManager.tsx
@@ -257,98 +257,8 @@ function FieldRow({
);
}
-// ── Tabellen-Gerüst ────────────────────────────────────────────────────────
-
-/** Eine Spaltendefinition: Titel-Key + optionale rechtsbündige Ausrichtung. */
-interface ResColumn {
- /** i18n-Key des Spaltentitels (leerer String = titellose Spalte). */
- titleKey: string;
- /** Zahlen-Spalten rechtsbündig ausrichten (Titel + Zellen). */
- align?: "right";
- /** Optionaler i18n-Key für den Tooltip auf dem Spaltentitel. */
- hintKey?: string;
-}
-
-/**
- * Tabelle als CSS-Grid: eine sticky Kopfzeile mit Spaltentiteln, darunter eine
- * Datenzeile je Item. Kopf und Zeilen erben `gridTemplateColumns` vom äußeren
- * Grid, damit die Spalten exakt fluchten. Datenzeilen werden als Kinder
- * übergeben (jede Zeile = `ResRow`).
- */
-function ResTable({
- columns,
- template,
- children,
-}: {
- columns: ResColumn[];
- template: string;
- children: React.ReactNode;
-}) {
- return (
-
-
- {columns.map((c, i) => (
-
- {c.titleKey ? t(c.titleKey) : ""}
-
- ))}
-
- {children}
-
- );
-}
-
-/** Eine Datenzeile der Tabelle (Zellen als Kinder, eine je Spalte). */
-function ResRow({ children }: { children: React.ReactNode }) {
- return {children}
;
-}
-
-/** Eine Tabellenzelle; optional rechtsbündig (für Zahlen). */
-function ResCell({
- children,
- align,
- emphasis = false,
-}: {
- children: React.ReactNode;
- align?: "right";
- emphasis?: boolean;
-}) {
- return (
-
- {children}
-
- );
-}
-
// ── Bauteile (Components) ──────────────────────────────────────────────────
-// Spalten: Name | Vordergrund | Hintergrund | Schnittschraffur | Ansichtsschraffur | Prio | Textur | Material | (löschen).
-const COMPONENT_COLUMNS: ResColumn[] = [
- { titleKey: "resources.col.name" },
- { titleKey: "resources.col.foreground", hintKey: "resources.col.foreground.hint" },
- { titleKey: "resources.col.background", hintKey: "resources.col.background.hint" },
- { titleKey: "resources.col.hatch" },
- { titleKey: "resources.col.viewHatch" },
- {
- titleKey: "resources.col.prio",
- align: "right",
- hintKey: "resources.col.prio.hint",
- },
- { titleKey: "resources.col.texture" },
- { titleKey: "resources.col.material" },
- { titleKey: "" },
-];
-const COMPONENT_TEMPLATE =
- "minmax(120px, 1fr) auto auto minmax(120px, 0.8fr) minmax(120px, 0.8fr) 64px minmax(90px, 0.7fr) minmax(130px, 0.9fr) 36px";
-
/**
* Material-Zelle der Bauteil-Tabelle: ein Button, der den aktuellen Material-
* Namen zeigt (oder „ohne") und ein Auswahl-Modal öffnet. Die Zuweisung läuft
@@ -803,7 +713,24 @@ function ImagePickButton({ onFile }: { onFile: (file: File | null) => void }) {
);
}
-function ComponentRow({
+/** Kleiner Farb-Swatch als Listen-Thumbnail für Bauteile ohne Schraffur. */
+function ColorSwatch({ color, size = 30 }: { color: string; size?: number }) {
+ return (
+
+ );
+}
+
+/** Detail-Panel EINES Bauteils (rechte Seite des Master-Detail-Layouts). */
+function ComponentDetail({
component,
hatches,
onPatch,
@@ -820,74 +747,91 @@ function ComponentRow({
{ value: "", label: t("resources.viewHatch.none") },
...hatchOptions,
];
+ const previewHatch = hatches.find((h) => h.id === component.hatchId);
return (
-
-
-
+
+ onPatch({ name })}
placeholder={t("resources.components.placeholder")}
+ onChange={(e) => onPatch({ name: e.target.value })}
/>
-
-
- {/* Vordergrund = Farbe der Muster-/Schraffurlinien. Fehlt sie, greift
- als Anzeige-Fallback `color`; das Setzen definiert `foreground`. */}
- onPatch({ foreground })}
- />
-
-
- {/* Hintergrund = Füllung (Poché). Fehlt sie, gilt `color` als Fallback. */}
- onPatch({ background })}
- />
-
-
- onPatch({ hatchId })}
- options={hatchOptions}
- />
-
-
- onPatch({ viewHatchId: viewHatchId || undefined })}
- options={viewHatchOptions}
- />
-
-
- onPatch({ joinPriority })}
- step={10}
- min={0}
- />
-
-
- onPatch({ texture3d: v || undefined })}
- placeholder={t("resources.texture.empty")}
- mono
- />
-
-
- onPatch({ material })}
- onClear={() => onPatch({ material: undefined })}
- />
-
-
-
-
+
+
+
+ {previewHatch ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {/* Vordergrund = Farbe der Muster-/Schraffurlinien. Fehlt sie, greift
+ als Anzeige-Fallback `color`; das Setzen definiert `foreground`. */}
+ onPatch({ foreground })}
+ />
+
+
+ {/* Hintergrund = Füllung (Poché). Fehlt sie, gilt `color` als Fallback. */}
+ onPatch({ background })}
+ />
+
+
+ onPatch({ hatchId })}
+ options={hatchOptions}
+ />
+
+
+ onPatch({ viewHatchId: viewHatchId || undefined })}
+ options={viewHatchOptions}
+ />
+
+
+ onPatch({ joinPriority })}
+ step={10}
+ min={0}
+ />
+
+
+ onPatch({ texture3d: v || undefined })}
+ placeholder={t("resources.texture.empty")}
+ mono
+ />
+
+
+ onPatch({ material })}
+ onClear={() => onPatch({ material: undefined })}
+ />
+
+
+
);
}
@@ -902,24 +846,58 @@ function ComponentsTab({
onAddComponent: () => void;
onDeleteComponent: (id: string) => void;
}) {
+ const [selectedId, setSelectedId] = useState(null);
+ const components = project.components;
+ // Auswahl gegen die Liste validieren (nach Delete/Add stabil bleiben).
+ const selected = components.find((c) => c.id === selectedId) ?? components[0];
+
return (
{t("resources.components.hint")}
-
- {project.components.map((c) => (
- onPatchComponent(c.id, patch)}
- onDelete={() => onDeleteComponent(c.id)}
- />
- ))}
- {project.components.length === 0 && (
-
- )}
-
-
+
+
+ {components.map((c) => {
+ const hatch = project.hatches.find((h) => h.id === c.hatchId);
+ return (
+
setSelectedId(c.id)}
+ >
+
+ {hatch ? (
+
+ ) : (
+
+ )}
+
+ {c.name}
+
+ );
+ })}
+ {components.length === 0 && (
+
{t("resources.components.empty")}
+ )}
+
+
+ + {t("resources.add")}
+
+
+
+
+ {selected ? (
+
onPatchComponent(selected.id, patch)}
+ onDelete={() => onDeleteComponent(selected.id)}
+ />
+ ) : (
+ {t("resources.components.empty")}
+ )}
+
+
);
}
@@ -2050,16 +2028,6 @@ function MaterialsTab() {
// ── Gemeinsame kleine Bausteine ────────────────────────────────────────────
-function AddButton({ label, onClick }: { label: string; onClick: () => void }) {
- return (
-
-
- + {label}
-
-
- );
-}
-
function EmptyState({ text }: { text: string }) {
return {text}
;
}