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.
181 lines
4.8 KiB
TypeScript
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",
|
|
};
|