Files
layonara-forge/packages/frontend/src/components/gff/ItemEditor.tsx
T

209 lines
6.6 KiB
TypeScript

import { useMemo } from "react";
import {
GffEditor,
GffFieldType,
getFieldValue,
getLocStringText,
type FieldOverrideProps,
} from "./GffEditor";
interface ItemEditorProps {
repo: string;
filePath: string;
content: string;
onSave?: (content: string) => void;
onSwitchToRaw?: () => void;
}
function BaseItemOverride({ value, onChange, field }: FieldOverrideProps) {
const num = typeof value === "number" ? value : 0;
return (
<div className="flex items-center gap-3">
<label className="w-44 shrink-0 text-sm" style={{ color: "var(--forge-text-secondary)" }}>
{field.displayName}
</label>
<input
type="number"
value={num}
onChange={(e) => onChange(field.label, parseInt(e.target.value, 10))}
className="w-24 rounded border px-2 py-1.5 text-sm"
style={{
backgroundColor: "var(--forge-bg)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
/>
<span className="text-xs" style={{ color: "var(--forge-text-secondary)" }}>
(baseitems.2da row)
</span>
</div>
);
}
function CompactNumbersOverride({ value, onChange, field, data }: FieldOverrideProps) {
const stackSize = typeof getFieldValue(data, "StackSize") === "number"
? (getFieldValue(data, "StackSize") as number) : 0;
const cost = typeof getFieldValue(data, "Cost") === "number"
? (getFieldValue(data, "Cost") as number) : 0;
const charges = typeof getFieldValue(data, "Charges") === "number"
? (getFieldValue(data, "Charges") as number) : 0;
if (field.label !== "StackSize") return null;
return (
<div className="flex items-center gap-4">
{[
{ label: "StackSize", display: "Stack", value: stackSize, max: 99 },
{ label: "Cost", display: "Cost (gp)", value: cost, max: 999999 },
{ label: "Charges", display: "Charges", value: charges, max: 255 },
].map((item) => (
<div key={item.label} className="flex items-center gap-2">
<label className="text-sm" style={{ color: "var(--forge-text-secondary)" }}>
{item.display}
</label>
<input
type="number"
value={item.value}
min={0}
max={item.max}
onChange={(e) => onChange(item.label, parseInt(e.target.value, 10))}
className="w-24 rounded border px-2 py-1.5 text-sm"
style={{
backgroundColor: "var(--forge-bg)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
/>
</div>
))}
</div>
);
}
function BooleanFlagsOverride({ data, onChange }: FieldOverrideProps) {
const flags = [
{ label: "Identified", display: "Identified" },
{ label: "Plot", display: "Plot" },
{ label: "Stolen", display: "Stolen" },
{ label: "Cursed", display: "Cursed" },
];
return (
<div className="flex items-center gap-6">
{flags.map((flag) => {
const val = getFieldValue(data, flag.label);
const checked = typeof val === "number" ? val !== 0 : Boolean(val);
return (
<label key={flag.label} className="flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(flag.label, e.target.checked ? 1 : 0)}
className="h-4 w-4 rounded"
style={{ accentColor: "var(--forge-accent)" }}
/>
<span style={{ color: "var(--forge-text)" }}>{flag.display}</span>
</label>
);
})}
</div>
);
}
function PropertiesListOverride({ value }: FieldOverrideProps) {
const list = Array.isArray(value) ? value : [];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm" style={{ color: "var(--forge-text)" }}>
Item Properties
</span>
<div className="flex gap-2">
<button
className="rounded px-2 py-1 text-xs"
style={{ backgroundColor: "var(--forge-bg)", color: "var(--forge-text-secondary)" }}
>
+ Add Property
</button>
</div>
</div>
{list.map((prop, i) => (
<div
key={i}
className="flex items-center gap-2 rounded border px-3 py-2"
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-bg)" }}
>
<span className="flex-1 font-mono text-xs" style={{ color: "var(--forge-text)" }}>
{typeof prop === "object" && prop !== null
? JSON.stringify(prop).slice(0, 80)
: String(prop)}
</span>
<button
className="text-xs"
style={{ color: "#ef4444" }}
>
Remove
</button>
</div>
))}
{list.length === 0 && (
<p className="py-2 text-xs" style={{ color: "var(--forge-text-secondary)" }}>
No item properties
</p>
)}
</div>
);
}
export function ItemEditor({ repo, filePath, content, onSave, onSwitchToRaw }: ItemEditorProps) {
const fieldOverrides = useMemo(() => {
const overrides = new Map<string, (props: FieldOverrideProps) => React.ReactNode>();
overrides.set("BaseItem", (props) => <BaseItemOverride {...props} />);
overrides.set("StackSize", (props) => <CompactNumbersOverride {...props} />);
overrides.set("Cost", () => null);
overrides.set("Charges", () => null);
overrides.set("Identified", (props) => <BooleanFlagsOverride {...props} />);
overrides.set("Plot", () => null);
overrides.set("Stolen", () => null);
overrides.set("Cursed", () => null);
overrides.set("PropertiesList", (props) => <PropertiesListOverride {...props} />);
return overrides;
}, []);
const itemName = useMemo(() => {
try {
const data = JSON.parse(content);
const nameField = data.LocalizedName;
return getLocStringText(nameField) || "(unnamed item)";
} catch {
return "(unnamed item)";
}
}, [content]);
const headerSlot = (
<div
className="border-b px-4 py-3"
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-surface)" }}
>
<h2 className="text-lg font-semibold" style={{ color: "var(--forge-text)" }}>
{itemName}
</h2>
</div>
);
return (
<GffEditor
repo={repo}
filePath={filePath}
content={content}
onSave={onSave}
onSwitchToRaw={onSwitchToRaw}
fieldOverrides={fieldOverrides}
headerSlot={headerSlot}
/>
);
}