diff --git a/components/Inventory.tsx b/components/Inventory.tsx
index 6cafc53..dd0278c 100644
--- a/components/Inventory.tsx
+++ b/components/Inventory.tsx
@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from "react";
-import * as XLSX from "xlsx";
+import ExcelJS from "exceljs";
import { chemicalsApi } from "../lib/api";
import type { ChemicalInventory } from "../shared/types";
import { Card } from "./ui/card";
@@ -282,15 +282,19 @@ export function Inventory() {
URL.revokeObjectURL(url);
}
- function handleDownloadExcel() {
- const wsData = [
- TABLE_COLUMNS.map(c => c.label),
- ...filtered.map(item => TABLE_COLUMNS.map(c => item[c.key] ?? "")),
- ];
- const ws = XLSX.utils.aoa_to_sheet(wsData);
- const wb = XLSX.utils.book_new();
- XLSX.utils.book_append_sheet(wb, ws, "Chemical Inventory");
- XLSX.writeFile(wb, "chemical_inventory.xlsx");
+ async function handleDownloadExcel() {
+ const wb = new ExcelJS.Workbook();
+ const ws = wb.addWorksheet("Chemical Inventory");
+ ws.addRow(TABLE_COLUMNS.map(c => c.label));
+ filtered.forEach(item => ws.addRow(TABLE_COLUMNS.map(c => item[c.key] ?? "")));
+ const buffer = await wb.xlsx.writeBuffer();
+ const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "chemical_inventory.xlsx";
+ a.click();
+ URL.revokeObjectURL(url);
}
// ── derived ─────────────────────────────────────────────────────────────
@@ -622,7 +626,7 @@ export function Inventory() {
-
+
diff --git a/package.json b/package.json
index baaea8f..b161a7d 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"cmdk": "^1.1.1",
"dotenv": "^17.3.1",
"embla-carousel-react": "^8.6.0",
+ "exceljs": "^4.4.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"next-themes": "^0.4.6",
@@ -53,8 +54,7 @@
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "^2.6.0",
- "vaul": "^1.1.2",
- "xlsx": "^0.18.5"
+ "vaul": "^1.1.2"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
diff --git a/styles/globals.css b/styles/globals.css
index 9030b3f..83bdb17 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -197,4 +197,92 @@
html {
font-size: var(--font-size);
+}
+
+.table-scroll {
+ overflow: scroll;
+ flex: 1;
+}
+
+.table-scroll::-webkit-scrollbar {
+ width: 14px;
+ height: 14px;
+}
+
+.table-scroll::-webkit-scrollbar-track {
+ background: var(--muted);
+ border: 1px solid var(--border);
+}
+
+.table-scroll::-webkit-scrollbar-thumb {
+ background: var(--muted-foreground);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ min-width: 30px;
+ min-height: 30px;
+}
+
+.table-scroll::-webkit-scrollbar-thumb:hover {
+ background: var(--foreground);
+}
+
+/* Hide all buttons by default, then selectively show the correct one per end */
+.table-scroll::-webkit-scrollbar-button {
+ display: none;
+}
+
+/* Up arrow — top of vertical bar */
+.table-scroll::-webkit-scrollbar-button:vertical:decrement:start {
+ display: block;
+ background: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 999px 999px 0 0;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M4 2 L7 6 L1 6 Z' fill='%23666'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 8px 8px;
+}
+
+/* Down arrow — bottom of vertical bar */
+.table-scroll::-webkit-scrollbar-button:vertical:increment:end {
+ display: block;
+ background: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 0 0 999px 999px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M4 6 L7 2 L1 2 Z' fill='%23666'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 8px 8px;
+}
+
+/* Left arrow — left end of horizontal bar */
+.table-scroll::-webkit-scrollbar-button:horizontal:decrement:start {
+ display: block;
+ background: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 999px 0 0 999px;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M2 4 L6 1 L6 7 Z' fill='%23666'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 8px 8px;
+}
+
+/* Right arrow — right end of horizontal bar */
+.table-scroll::-webkit-scrollbar-button:horizontal:increment:end {
+ display: block;
+ background: var(--muted);
+ border: 1px solid var(--border);
+ border-radius: 0 999px 999px 0;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath d='M6 4 L2 1 L2 7 Z' fill='%23666'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 8px 8px;
+}
+
+.table-scroll::-webkit-scrollbar-button:hover {
+ background: var(--border);
+}
+
+.table-scroll::-webkit-scrollbar-corner {
+ background: transparent;
}
\ No newline at end of file