- Replace 0.3rem padding/margin/gap with 0.25rem (xs) - Replace 0.2rem margins with 0.25rem (xs) - Replace 0.1rem padding with 0.15rem (badge spec) - Replace 0.6rem margins/padding with 0.5rem (sm) - Fix borderRadius 0.3rem to 0.375rem (6px per style guide) - Preserve style-guide-specified values: 0.35rem button gap, 0.4rem cell padding, 0.45rem input padding
210 lines
5.8 KiB
TypeScript
210 lines
5.8 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { X } from "lucide-react";
|
|
import { get } from "../../api/client";
|
|
import type { Item } from "../../api/types";
|
|
import { MainTab } from "./MainTab";
|
|
import { PropertiesTab } from "./PropertiesTab";
|
|
import { RevisionsTab } from "./RevisionsTab";
|
|
import { BOMTab } from "./BOMTab";
|
|
import { WhereUsedTab } from "./WhereUsedTab";
|
|
|
|
type Tab = "main" | "properties" | "revisions" | "bom" | "where-used";
|
|
|
|
interface ItemDetailProps {
|
|
partNumber: string;
|
|
onClose: () => void;
|
|
onEdit: (pn: string) => void;
|
|
onDelete: (pn: string) => void;
|
|
onReload: () => void;
|
|
isEditor: boolean;
|
|
}
|
|
|
|
const tabs: { key: Tab; label: string }[] = [
|
|
{ key: "main", label: "Main" },
|
|
{ key: "properties", label: "Properties" },
|
|
{ key: "revisions", label: "Revisions" },
|
|
{ key: "bom", label: "BOM" },
|
|
{ key: "where-used", label: "Where Used" },
|
|
];
|
|
|
|
export function ItemDetail({
|
|
partNumber,
|
|
onClose,
|
|
onEdit,
|
|
onDelete,
|
|
onReload,
|
|
isEditor,
|
|
}: ItemDetailProps) {
|
|
const [item, setItem] = useState<Item | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState<Tab>("main");
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
setActiveTab("main");
|
|
get<Item>(`/api/items/${encodeURIComponent(partNumber)}?include=properties`)
|
|
.then(setItem)
|
|
.catch(() => setItem(null))
|
|
.finally(() => setLoading(false));
|
|
}, [partNumber]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ padding: "1rem", color: "var(--ctp-subtext0)" }}>
|
|
Loading...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!item) {
|
|
return (
|
|
<div style={{ padding: "1rem", color: "var(--ctp-red)" }}>
|
|
Item not found
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const typeColors: Record<string, { bg: string; color: string }> = {
|
|
part: { bg: "rgba(166,227,161,0.2)", color: "var(--ctp-green)" },
|
|
assembly: { bg: "rgba(203,166,247,0.2)", color: "var(--ctp-mauve)" },
|
|
document: { bg: "rgba(137,180,250,0.2)", color: "var(--ctp-blue)" },
|
|
purchased: { bg: "rgba(250,179,135,0.2)", color: "var(--ctp-peach)" },
|
|
phantom: { bg: "rgba(127,132,156,0.2)", color: "var(--ctp-overlay1)" },
|
|
tooling: { bg: "rgba(243,139,168,0.2)", color: "var(--ctp-red)" },
|
|
};
|
|
const tc = typeColors[item.item_type] ?? {
|
|
bg: "var(--ctp-surface1)",
|
|
color: "var(--ctp-text)",
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
|
{/* Header */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "0.75rem",
|
|
padding: "0.5rem 0.75rem",
|
|
borderBottom: "1px solid var(--ctp-surface1)",
|
|
backgroundColor: "var(--ctp-mantle)",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontFamily: "'JetBrains Mono', monospace",
|
|
color: "var(--ctp-peach)",
|
|
fontWeight: 600,
|
|
fontSize: "var(--font-body)",
|
|
}}
|
|
>
|
|
{item.part_number}
|
|
</span>
|
|
<span
|
|
style={{
|
|
padding: "0.15rem 0.5rem",
|
|
borderRadius: "1rem",
|
|
fontSize: "var(--font-sm)",
|
|
fontWeight: 500,
|
|
backgroundColor: tc.bg,
|
|
color: tc.color,
|
|
}}
|
|
>
|
|
{item.item_type}
|
|
</span>
|
|
<span style={{ flex: 1 }} />
|
|
{isEditor && (
|
|
<>
|
|
<button
|
|
onClick={() => onEdit(item.part_number)}
|
|
style={headerBtnStyle}
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => onDelete(item.part_number)}
|
|
style={{ ...headerBtnStyle, color: "var(--ctp-red)" }}
|
|
>
|
|
Delete
|
|
</button>
|
|
</>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
style={{
|
|
...headerBtnStyle,
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
gap: "0",
|
|
borderBottom: "1px solid var(--ctp-surface1)",
|
|
backgroundColor: "var(--ctp-mantle)",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.key}
|
|
onClick={() => setActiveTab(tab.key)}
|
|
style={{
|
|
padding: "0.4rem 0.75rem",
|
|
fontSize: "var(--font-table)",
|
|
border: "none",
|
|
borderBottom:
|
|
activeTab === tab.key
|
|
? "2px solid var(--ctp-mauve)"
|
|
: "2px solid transparent",
|
|
backgroundColor: "transparent",
|
|
color:
|
|
activeTab === tab.key
|
|
? "var(--ctp-mauve)"
|
|
: "var(--ctp-subtext0)",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
<div style={{ flex: 1, overflow: "auto", padding: "0.75rem" }}>
|
|
{activeTab === "main" && (
|
|
<MainTab item={item} onReload={onReload} isEditor={isEditor} />
|
|
)}
|
|
{activeTab === "properties" && (
|
|
<PropertiesTab item={item} onReload={onReload} isEditor={isEditor} />
|
|
)}
|
|
{activeTab === "revisions" && (
|
|
<RevisionsTab partNumber={item.part_number} isEditor={isEditor} />
|
|
)}
|
|
{activeTab === "bom" && (
|
|
<BOMTab partNumber={item.part_number} isEditor={isEditor} />
|
|
)}
|
|
{activeTab === "where-used" && (
|
|
<WhereUsedTab partNumber={item.part_number} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const headerBtnStyle: React.CSSProperties = {
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
color: "var(--ctp-subtext1)",
|
|
fontSize: "var(--font-table)",
|
|
padding: "0.25rem 0.4rem",
|
|
};
|