ResourceManager: Bauteile-Tab auf Master-Detail-Layout umgestellt

Wie bei Schraffuren/Linien/Wandstile: Liste links, Detail-Panel rechts.
Vorarbeit vor der eigentlichen Bauteil-Logik. Alte Tabellen-Bausteine
(ResTable/ResRow/ResCell/AddButton) entfernt, da nicht mehr genutzt.
This commit is contained in:
2026-07-04 04:25:11 +02:00
parent 7b58745855
commit 368a2ffcdb
+108 -140
View File
@@ -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 (
<div className="res-table" style={{ gridTemplateColumns: template }}>
<div className="res-thead">
{columns.map((c, i) => (
<div
key={i}
className={`res-th${c.align === "right" ? " num" : ""}`}
title={c.hintKey ? t(c.hintKey) : undefined}
>
{c.titleKey ? t(c.titleKey) : ""}
</div>
))}
</div>
{children}
</div>
);
}
/** Eine Datenzeile der Tabelle (Zellen als Kinder, eine je Spalte). */
function ResRow({ children }: { children: React.ReactNode }) {
return <div className="res-tr">{children}</div>;
}
/** Eine Tabellenzelle; optional rechtsbündig (für Zahlen). */
function ResCell({
children,
align,
emphasis = false,
}: {
children: React.ReactNode;
align?: "right";
emphasis?: boolean;
}) {
return (
<div
className={`res-td${align === "right" ? " num" : ""}${
emphasis ? " emphasis" : ""
}`}
>
{children}
</div>
);
}
// ── Bauteile (Components) ────────────────────────────────────────────────── // ── 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- * Material-Zelle der Bauteil-Tabelle: ein Button, der den aktuellen Material-
* Namen zeigt (oder „ohne") und ein Auswahl-Modal öffnet. Die Zuweisung läuft * 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 (
<span
className="res-swatch"
style={{
display: "block",
width: size,
height: size,
background: color,
border: "1px solid var(--border)",
}}
/>
);
}
/** Detail-Panel EINES Bauteils (rechte Seite des Master-Detail-Layouts). */
function ComponentDetail({
component, component,
hatches, hatches,
onPatch, onPatch,
@@ -820,74 +747,91 @@ function ComponentRow({
{ value: "", label: t("resources.viewHatch.none") }, { value: "", label: t("resources.viewHatch.none") },
...hatchOptions, ...hatchOptions,
]; ];
const previewHatch = hatches.find((h) => h.id === component.hatchId);
return ( return (
<ResRow> <div className="res-md-detail-inner">
<ResCell> <div className="res-md-head">
<TextField <input
className="res-input res-md-rename"
value={component.name} value={component.name}
onChange={(name) => onPatch({ name })}
placeholder={t("resources.components.placeholder")} placeholder={t("resources.components.placeholder")}
onChange={(e) => onPatch({ name: e.target.value })}
/> />
</ResCell> <DeleteButton
<ResCell> onClick={onDelete}
title={t("resources.delete.component", { name: component.name })}
/>
</div>
<div className="res-md-preview">
{previewHatch ? (
<HatchSwatch hatch={previewHatch} size={128} />
) : (
<ColorSwatch color={component.foreground ?? component.color} size={128} />
)}
</div>
<div className="res-fields">
<FieldRow
label={t("resources.col.foreground")}
hint={t("resources.col.foreground.hint")}
>
{/* Vordergrund = Farbe der Muster-/Schraffurlinien. Fehlt sie, greift {/* Vordergrund = Farbe der Muster-/Schraffurlinien. Fehlt sie, greift
als Anzeige-Fallback `color`; das Setzen definiert `foreground`. */} als Anzeige-Fallback `color`; das Setzen definiert `foreground`. */}
<ColorField <ColorField
color={component.foreground ?? component.color} color={component.foreground ?? component.color}
onChange={(foreground) => onPatch({ foreground })} onChange={(foreground) => onPatch({ foreground })}
/> />
</ResCell> </FieldRow>
<ResCell> <FieldRow
label={t("resources.col.background")}
hint={t("resources.col.background.hint")}
>
{/* Hintergrund = Füllung (Poché). Fehlt sie, gilt `color` als Fallback. */} {/* Hintergrund = Füllung (Poché). Fehlt sie, gilt `color` als Fallback. */}
<ColorField <ColorField
color={component.background ?? component.color} color={component.background ?? component.color}
onChange={(background) => onPatch({ background })} onChange={(background) => onPatch({ background })}
/> />
</ResCell> </FieldRow>
<ResCell> <FieldRow label={t("resources.col.hatch")}>
<SelectField <SelectField
value={component.hatchId} value={component.hatchId}
onChange={(hatchId) => onPatch({ hatchId })} onChange={(hatchId) => onPatch({ hatchId })}
options={hatchOptions} options={hatchOptions}
/> />
</ResCell> </FieldRow>
<ResCell> <FieldRow label={t("resources.col.viewHatch")}>
<SelectField <SelectField
value={component.viewHatchId ?? ""} value={component.viewHatchId ?? ""}
onChange={(viewHatchId) => onPatch({ viewHatchId: viewHatchId || undefined })} onChange={(viewHatchId) => onPatch({ viewHatchId: viewHatchId || undefined })}
options={viewHatchOptions} options={viewHatchOptions}
/> />
</ResCell> </FieldRow>
<ResCell align="right" emphasis> <FieldRow label={t("resources.col.prio")} hint={t("resources.col.prio.hint")}>
<NumberField <NumberField
value={component.joinPriority} value={component.joinPriority}
onChange={(joinPriority) => onPatch({ joinPriority })} onChange={(joinPriority) => onPatch({ joinPriority })}
step={10} step={10}
min={0} min={0}
/> />
</ResCell> </FieldRow>
<ResCell> <FieldRow label={t("resources.col.texture")}>
<TextField <TextField
value={component.texture3d ?? ""} value={component.texture3d ?? ""}
onChange={(v) => onPatch({ texture3d: v || undefined })} onChange={(v) => onPatch({ texture3d: v || undefined })}
placeholder={t("resources.texture.empty")} placeholder={t("resources.texture.empty")}
mono mono
/> />
</ResCell> </FieldRow>
<ResCell> <FieldRow label={t("resources.col.material")}>
<MaterialCell <MaterialCell
material={component.material} material={component.material}
onAssign={(material) => onPatch({ material })} onAssign={(material) => onPatch({ material })}
onClear={() => onPatch({ material: undefined })} onClear={() => onPatch({ material: undefined })}
/> />
</ResCell> </FieldRow>
<ResCell> </div>
<DeleteButton </div>
onClick={onDelete}
title={t("resources.delete.component", { name: component.name })}
/>
</ResCell>
</ResRow>
); );
} }
@@ -902,24 +846,58 @@ function ComponentsTab({
onAddComponent: () => void; onAddComponent: () => void;
onDeleteComponent: (id: string) => void; onDeleteComponent: (id: string) => void;
}) { }) {
const [selectedId, setSelectedId] = useState<string | null>(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 ( return (
<div className="res-tab"> <div className="res-tab">
<div className="res-hint">{t("resources.components.hint")}</div> <div className="res-hint">{t("resources.components.hint")}</div>
<ResTable columns={COMPONENT_COLUMNS} template={COMPONENT_TEMPLATE}> <div className="res-md">
{project.components.map((c) => ( <div className="res-md-list">
<ComponentRow {components.map((c) => {
const hatch = project.hatches.find((h) => h.id === c.hatchId);
return (
<button
key={c.id} key={c.id}
component={c} className={`res-md-row${selected?.id === c.id ? " active" : ""}`}
hatches={project.hatches} onClick={() => setSelectedId(c.id)}
onPatch={(patch) => onPatchComponent(c.id, patch)} >
onDelete={() => onDeleteComponent(c.id)} <span className="res-md-row-thumb">
/> {hatch ? (
))} <HatchSwatch hatch={hatch} size={30} />
{project.components.length === 0 && ( ) : (
<EmptyState text={t("resources.components.empty")} /> <ColorSwatch color={c.foreground ?? c.color} />
)} )}
</ResTable> </span>
<AddButton label={t("resources.add")} onClick={onAddComponent} /> <span className="res-md-row-name">{c.name}</span>
</button>
);
})}
{components.length === 0 && (
<div className="res-md-empty">{t("resources.components.empty")}</div>
)}
<div className="res-md-listfoot">
<button className="res-add" onClick={onAddComponent}>
<span className="res-add-plus">+</span> {t("resources.add")}
</button>
</div>
</div>
<div className="res-md-detail">
{selected ? (
<ComponentDetail
key={selected.id}
component={selected}
hatches={project.hatches}
onPatch={(patch) => onPatchComponent(selected.id, patch)}
onDelete={() => onDeleteComponent(selected.id)}
/>
) : (
<div className="res-md-empty">{t("resources.components.empty")}</div>
)}
</div>
</div>
</div> </div>
); );
} }
@@ -2050,16 +2028,6 @@ function MaterialsTab() {
// ── Gemeinsame kleine Bausteine ──────────────────────────────────────────── // ── Gemeinsame kleine Bausteine ────────────────────────────────────────────
function AddButton({ label, onClick }: { label: string; onClick: () => void }) {
return (
<div className="res-add-row">
<button className="res-add" onClick={onClick}>
<span className="res-add-plus">+</span> {label}
</button>
</div>
);
}
function EmptyState({ text }: { text: string }) { function EmptyState({ text }: { text: string }) {
return <div className="res-empty">{text}</div>; return <div className="res-empty">{text}</div>;
} }