Files
silo/web/src/components/items/ItemsToolbar.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

181 lines
4.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { get } from "../../api/client";
import type { Project } from "../../api/types";
import type { ItemFilters } from "../../hooks/useItems";
interface ItemsToolbarProps {
filters: ItemFilters;
onFilterChange: (partial: Partial<ItemFilters>) => void;
layout: "horizontal" | "vertical";
onLayoutChange: (layout: "horizontal" | "vertical") => void;
onExport: () => void;
onImport: () => void;
onCreate: () => void;
isEditor: boolean;
}
export function ItemsToolbar({
filters,
onFilterChange,
layout,
onLayoutChange,
onExport,
onImport,
onCreate,
isEditor,
}: ItemsToolbarProps) {
const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => {
get<Project[]>("/api/projects")
.then(setProjects)
.catch(() => {});
}, []);
const scopeBtn = (scope: ItemFilters["searchScope"], label: string) => (
<button
onClick={() => onFilterChange({ searchScope: scope })}
style={{
padding: "var(--d-input-py) var(--d-input-px)",
fontSize: "var(--d-input-font)",
border: "none",
borderRadius: "0.3rem",
cursor: "pointer",
backgroundColor:
filters.searchScope === scope
? "var(--ctp-mauve)"
: "var(--ctp-surface1)",
color:
filters.searchScope === scope
? "var(--ctp-crust)"
: "var(--ctp-subtext1)",
}}
>
{label}
</button>
);
return (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "var(--d-toolbar-gap)",
alignItems: "center",
padding: "var(--d-toolbar-py) 0",
borderBottom: "1px solid var(--ctp-surface0)",
marginBottom: "var(--d-toolbar-mb)",
}}
>
{/* Search */}
<input
type="text"
placeholder="Search items... (Ctrl+F)"
value={filters.search}
onChange={(e) => onFilterChange({ search: e.target.value })}
style={{
flex: 1,
minWidth: 200,
padding: "var(--d-input-py) var(--d-input-px)",
backgroundColor: "var(--ctp-surface0)",
border: "1px solid var(--ctp-surface1)",
borderRadius: "0.4rem",
color: "var(--ctp-text)",
fontSize: "var(--d-input-font)",
}}
/>
{/* Search scope */}
<div style={{ display: "flex", gap: "0.25rem" }}>
{scopeBtn("all", "All")}
{scopeBtn("part_number", "PN")}
{scopeBtn("description", "Desc")}
</div>
{/* Type filter */}
<select
value={filters.type}
onChange={(e) => onFilterChange({ type: e.target.value })}
style={selectStyle}
>
<option value="">All Types</option>
<option value="part">Part</option>
<option value="assembly">Assembly</option>
<option value="document">Document</option>
<option value="tooling">Tooling</option>
</select>
{/* Project filter */}
<select
value={filters.project}
onChange={(e) => onFilterChange({ project: e.target.value })}
style={selectStyle}
>
<option value="">All Projects</option>
{projects.map((p) => (
<option key={p.code} value={p.code}>
{p.code}
{p.name ? `${p.name}` : ""}
</option>
))}
</select>
{/* Layout toggle */}
<button
onClick={() =>
onLayoutChange(layout === "horizontal" ? "vertical" : "horizontal")
}
title={`Switch to ${layout === "horizontal" ? "vertical" : "horizontal"} layout`}
style={toolBtnStyle}
>
{layout === "horizontal" ? "⬌" : "⬍"}
</button>
{/* Export */}
<button onClick={onExport} style={toolBtnStyle} title="Export CSV">
Export
</button>
{/* Import (editor only) */}
{isEditor && (
<button onClick={onImport} style={toolBtnStyle} title="Import CSV">
Import
</button>
)}
{/* Create (editor only) */}
{isEditor && (
<button
onClick={onCreate}
style={{
...toolBtnStyle,
backgroundColor: "var(--ctp-mauve)",
color: "var(--ctp-crust)",
}}
>
+ New
</button>
)}
</div>
);
}
const selectStyle: React.CSSProperties = {
padding: "var(--d-input-py) var(--d-input-px)",
backgroundColor: "var(--ctp-surface0)",
border: "1px solid var(--ctp-surface1)",
borderRadius: "0.4rem",
color: "var(--ctp-text)",
fontSize: "var(--d-input-font)",
};
const toolBtnStyle: React.CSSProperties = {
padding: "var(--d-input-py) var(--d-input-px)",
backgroundColor: "var(--ctp-surface1)",
border: "none",
borderRadius: "0.4rem",
color: "var(--ctp-text)",
fontSize: "var(--d-input-font)",
cursor: "pointer",
};