Files
silo/web/src/components/audit/AuditTable.tsx
Forbes cb88b3977c feat(web): add user-selectable density mode with compact/comfortable toggle
Implements #17, #18, #19, #20, #21

- Add CSS custom properties for density-dependent spacing (--d-* vars)
  in theme.css with comfortable (default) and compact modes
- Create useDensity hook with localStorage persistence and DOM attribute sync
- Add FOUC prevention in main.tsx (sync density before first paint)
- Create shared PageFooter component merging stats + pagination
- Refactor AppShell to flex layout with density toggle button (COM/CMP)
- Consolidate inline pagination from ItemsPage/AuditPage into PageFooter
- Delete FooterStats.tsx (replaced by PageFooter)
- Replace all hardcoded padding/font/gap values in ItemTable, AuditTable,
  ItemsToolbar, and AuditToolbar with var(--d-*) references

Comfortable mode is already tighter than the previous hardcoded values.
Compact mode reduces further for power-user density.
2026-02-08 18:35:25 -06:00

171 lines
4.5 KiB
TypeScript

import type { AuditItemResult } from "../../api/types";
const tierColors: Record<string, string> = {
critical: "var(--ctp-red)",
low: "var(--ctp-peach)",
partial: "var(--ctp-yellow)",
good: "var(--ctp-green)",
complete: "var(--ctp-teal)",
};
interface AuditTableProps {
items: AuditItemResult[];
loading: boolean;
selectedPN: string | null;
onSelect: (pn: string) => void;
}
export function AuditTable({
items,
loading,
selectedPN,
onSelect,
}: AuditTableProps) {
if (loading) {
return (
<div
style={{
padding: "2rem",
color: "var(--ctp-subtext0)",
textAlign: "center",
}}
>
Loading audit data...
</div>
);
}
if (items.length === 0) {
return (
<div
style={{
padding: "2rem",
color: "var(--ctp-subtext0)",
textAlign: "center",
}}
>
No items found
</div>
);
}
return (
<div style={{ overflow: "auto", flex: 1 }}>
<table
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: "0.8rem",
}}
>
<thead>
<tr>
{[
"Score",
"Part Number",
"Description",
"Category",
"Sourcing",
"Missing",
].map((h) => (
<th key={h} style={thStyle}>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{items.map((item) => {
const color = tierColors[item.tier] ?? "var(--ctp-subtext0)";
const isSelected = selectedPN === item.part_number;
return (
<tr
key={item.part_number}
onClick={() => onSelect(item.part_number)}
style={{
cursor: "pointer",
backgroundColor: isSelected
? "var(--ctp-surface1)"
: "transparent",
transition: "background-color 0.15s",
}}
onMouseEnter={(e) => {
if (!isSelected)
e.currentTarget.style.backgroundColor =
"var(--ctp-surface0)";
}}
onMouseLeave={(e) => {
if (!isSelected)
e.currentTarget.style.backgroundColor = "transparent";
}}
>
<td style={tdStyle}>
<span
style={{
display: "inline-block",
padding: "0.15rem 0.5rem",
borderRadius: "1rem",
fontSize: "0.75rem",
fontWeight: 600,
backgroundColor: color,
opacity: 0.9,
color: "var(--ctp-crust)",
}}
>
{(item.score * 100).toFixed(0)}%
</span>
</td>
<td
style={{
...tdStyle,
fontFamily: "'JetBrains Mono', monospace",
color: "var(--ctp-peach)",
}}
>
{item.part_number}
</td>
<td
style={{
...tdStyle,
maxWidth: 300,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{item.description}
</td>
<td style={tdStyle}>{item.category_name || item.category}</td>
<td style={tdStyle}>{item.sourcing_type}</td>
<td style={{ ...tdStyle, textAlign: "center" }}>
{item.missing.length}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
const thStyle: React.CSSProperties = {
textAlign: "left",
padding: "var(--d-th-py) var(--d-th-px)",
fontSize: "var(--d-th-font)",
borderBottom: "1px solid var(--ctp-surface1)",
color: "var(--ctp-subtext0)",
fontWeight: 500,
position: "sticky",
top: 0,
backgroundColor: "var(--ctp-base)",
zIndex: 1,
};
const tdStyle: React.CSSProperties = {
padding: "var(--d-td-py) var(--d-td-px)",
fontSize: "var(--d-td-font)",
borderBottom: "1px solid var(--ctp-surface0)",
color: "var(--ctp-text)",
};