Files
silo/web/src/components/items/ItemDetail.tsx
Forbes 07c4aa1c28 fix(web): align spacing values to style guide grid (#71)
- 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
2026-02-13 14:37:40 -06:00

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",
};