{hasOverride &&
●}
diff --git a/frontend/src/components/SearchableSelect.js b/frontend/src/components/SearchableSelect.js
new file mode 100644
index 0000000..718a84f
--- /dev/null
+++ b/frontend/src/components/SearchableSelect.js
@@ -0,0 +1,148 @@
+/**
+ * SearchableSelect — Inline searchable dropdown for the Granite Loader Sheet.
+ *
+ * Supports:
+ * - Static options (small lists like EQUIP_STATUS)
+ * - Large searchable lists (RESPONSIBLE_TEAM, SITE_NAME, EQUIP_TEMPLATE)
+ * - Keyboard navigation (arrow keys, enter, escape)
+ * - Typeahead filtering
+ * - Portal-free (renders inline to avoid z-index issues in table cells)
+ */
+
+import React, { useState, useRef, useEffect, useCallback } from 'react';
+
+const DROPDOWN_STYLE = {
+ position: 'absolute',
+ top: '100%',
+ left: 0,
+ right: 0,
+ zIndex: 100,
+ background: '#0F172A',
+ border: '1px solid #7C3AED',
+ borderRadius: '0 0 0.375rem 0.375rem',
+ maxHeight: '180px',
+ overflowY: 'auto',
+ boxShadow: '0 8px 24px rgba(0, 0, 0, 0.5)',
+};
+
+const OPTION_STYLE = {
+ padding: '0.3rem 0.6rem',
+ fontSize: '0.7rem',
+ color: '#E2E8F0',
+ cursor: 'pointer',
+ fontFamily: "'JetBrains Mono', monospace",
+};
+
+const OPTION_HIGHLIGHT = {
+ ...OPTION_STYLE,
+ background: 'rgba(124, 58, 237, 0.2)',
+};
+
+export default function SearchableSelect({ value, options, onChange, onClose, placeholder, autoFocus }) {
+ const [filter, setFilter] = useState(value || '');
+ const [highlightIdx, setHighlightIdx] = useState(-1);
+ const [isOpen, setIsOpen] = useState(true);
+ const inputRef = useRef(null);
+ const listRef = useRef(null);
+
+ useEffect(() => {
+ if (autoFocus && inputRef.current) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+ }, [autoFocus]);
+
+ const filtered = useCallback(() => {
+ if (!filter.trim()) return options.slice(0, 50);
+ const lower = filter.toLowerCase();
+ return options.filter(o => o.toLowerCase().includes(lower)).slice(0, 50);
+ }, [filter, options]);
+
+ const filteredOptions = filtered();
+
+ // Scroll highlighted item into view
+ useEffect(() => {
+ if (highlightIdx >= 0 && listRef.current) {
+ const el = listRef.current.children[highlightIdx];
+ if (el) el.scrollIntoView({ block: 'nearest' });
+ }
+ }, [highlightIdx]);
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setHighlightIdx(prev => Math.min(prev + 1, filteredOptions.length - 1));
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setHighlightIdx(prev => Math.max(prev - 1, 0));
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (highlightIdx >= 0 && filteredOptions[highlightIdx]) {
+ onChange(filteredOptions[highlightIdx]);
+ } else if (filter.trim()) {
+ onChange(filter.trim());
+ }
+ if (onClose) onClose();
+ } else if (e.key === 'Escape') {
+ if (onClose) onClose();
+ } else if (e.key === 'Tab') {
+ // Accept current value on tab
+ if (highlightIdx >= 0 && filteredOptions[highlightIdx]) {
+ onChange(filteredOptions[highlightIdx]);
+ } else if (filter.trim()) {
+ onChange(filter.trim());
+ }
+ if (onClose) onClose();
+ }
+ };
+
+ const handleSelect = (opt) => {
+ onChange(opt);
+ if (onClose) onClose();
+ };
+
+ return (
+
+
{ setFilter(e.target.value); setHighlightIdx(-1); setIsOpen(true); }}
+ onKeyDown={handleKeyDown}
+ onBlur={() => {
+ // Delay close so click on option can fire
+ setTimeout(() => {
+ if (filter.trim() && filter.trim() !== value) {
+ onChange(filter.trim());
+ }
+ if (onClose) onClose();
+ }, 150);
+ }}
+ placeholder={placeholder || 'Search...'}
+ />
+ {isOpen && filteredOptions.length > 0 && (
+
+ {filteredOptions.map((opt, i) => (
+
setHighlightIdx(i)}
+ onMouseDown={(e) => { e.preventDefault(); handleSelect(opt); }}
+ >
+ {opt}
+
+ ))}
+ {filteredOptions.length === 50 && (
+
+ Type to filter more...
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/utils/graniteLoaderPicklists.js b/frontend/src/utils/graniteLoaderPicklists.js
new file mode 100644
index 0000000..4bfa714
--- /dev/null
+++ b/frontend/src/utils/graniteLoaderPicklists.js
@@ -0,0 +1,65 @@
+/**
+ * Granite Loader Sheet picklist values.
+ * Extracted from docs/Team_Device Loader.xlsx reference sheets.
+ * These values are used for searchable dropdowns in the LoaderModal.
+ */
+
+export const RESPONSIBLE_TEAMS = [
+ 'AE-LESS-ENDS', 'AE-TI-LESS-PIES', 'APVS-PCDS-DIGITAL-ACCESS', 'APVS-PCDS-DIGITAL-IDENTITY',
+ 'APVS-UNKNOWN', 'ARCHIVED', 'AVWO-NON-CHARTER', 'AVWO-UNKNOWN', 'CARD-ABANDONED-UNKNOWN',
+ 'CARD-UNKNOWN', 'CTEC-LAB-SQUAD', 'CTEC-UNKNOWN', 'FOE-FIELD OPS', 'FOE-FIELD OPS-ROC',
+ 'ISP-NDC-CENTENNIAL', 'ISP-NDC-CHARLOTTE', 'ISP-NDC-COUDERSPORT', 'ISP-NDC-SIMPSONVILLE',
+ 'ISP-UNKNOWN', 'IT', 'IT-DSSS-EDP', 'IT-SA-OPS', 'IT-SOC', 'MTG-CORE-ENG',
+ 'MTG-WSTC-SYSTEM CERTIFICATION', 'NEO-UNKNOWN', 'NOC-METRICS-DATA WAREHOUSE', 'NON-CHARTER',
+ 'NTS-AEO-ACCESS-ENG', 'NTS-AEO-ACCESS-OPS', 'NTS-AEO-INTELDEV', 'NTS-AEO-STEAM',
+ 'NTS-AVOC-AIA-ANALYTICS-OPS', 'NTS-AVOC-AIA-TOOLS', 'NTS-AVOC-AIA-TOOLS-CHANGEAUTO',
+ 'NTS-AVOC-AIA-TOOLS-PERFMANAG', 'NTS-AVOC-CVO', 'NTS-AVOC-OPSINTEL',
+ 'NTS-AVOC-OPSINTEL-NSIGHTS', 'NTS-AVOC-RC-KM', 'NTS-AVOC-RC-TICKETING',
+ 'NTS-AVOC-SCM-ISS', 'NTS-AVOC-SCM-SAPO', 'NTS-AVOC-VCO',
+ 'NTS-CPE-IRCS-DASDNS', 'NTS-CPE-IRCS-DATAENG', 'NTS-CPE-IRCS-DCMS',
+ 'NTS-CPE-IRCS-DEVICE-AUTOMATION', 'NTS-CPE-IRCS-DEVICE-OPERATIONS',
+ 'NTS-CPE-IRCS-INTERNETRELIABILITY', 'NTS-CPE-IRCS-SCP-OPERATIONS',
+ 'NTS-CPE-WIFIHWDEV-CPEHW-AUTOMATION', 'NTS-CVWO-VOICE-LAB', 'NTS-CVWO-VOICE-OPS',
+ 'NTS-CVWO-WIRELESS-HMNO-WHO', 'NTS-CVWO-WIRELESS-RANEA', 'NTS-CVWO-WIRELESS-WNBO',
+ 'NTS-CVWO-WIRELESS-WNO-MWF', 'NTS-CVWO-WIRELESS-WOP-WCO', 'NTS-ISP-NDC', 'NTS-ISP-OPS',
+ 'NTS-NEO-BB-IP', 'NTS-NEO-BB-OPTICAL', 'NTS-NEO-CORE-IP', 'NTS-NEO-CORE-OPTICAL',
+ 'NTS-NEO-IP-MGMT', 'NTS-NEO-OPSENG-LAB', 'NTS-NEO-OPSENG-TOOLS',
+ 'PRDCT-VSO-VDE-ADV-DEV', 'PRDCT-VSO-VDE-ADV-FOCUS', 'PRDCT-VSO-VDE-ADV-IPOIS',
+ 'PRDCT-VSO-VDE-ADV-PQI', 'PRDCT-VSO-VDE-ADV-SRTA', 'PRDCT-VSO-VDE-CI',
+ 'PRDCT-VSO-VDE-ENT', 'PRDCT-VSO-VDE-IPVENG', 'PRDCT-VSO-VDE-VCDT',
+ 'PRDCT-VSO-VDE-VOD-CMS', 'PRDCT-VSO-VDE-VOD-DEV', 'PRDCT-VSO-VDE-VOD-LAB',
+ 'PRDCT-VSO-VSW-AIS', 'PRDCT-VSO-VSW-CRESCENDO', 'PRDCT-VSO-VSW-ENTITLEMENTS',
+ 'PRDCT-VSO-VSW-GSD', 'PRDCT-VSO-VSW-IPVS', 'PRDCT-VSO-VSW-LANTERN',
+ 'PRDCT-VSO-VSW-LINEUPS', 'PRDCT-VSO-VSW-METADATA', 'PRDCT-VSO-VSW-NNS',
+ 'PRDCT-VSO-VSW-SETTINGS', 'PRDCT-VSO-VSW-SPECFLOW', 'PRDCT-VSO-VSW-SRE',
+ 'PRDCT-VSO-VSW-TEAM', 'PRDCT-VSO-VSW-TVE', 'PRDCT-VSO-VSW-VOD', 'PRDCT-VSO-VSW-VSI',
+ 'SB-EPS-ENTDATA', 'SDIT-CSD-ITLS-ACT', 'SDIT-CSD-ITLS-ENDS', 'SDIT-CSD-ITLS-LABOPS',
+ 'SDIT-CSD-ITLS-LNE', 'SDIT-CSD-ITLS-LPE', 'SDIT-CSD-ITLS-NSIE', 'SDIT-CSD-ITLS-PACE',
+ 'SDIT-CSD-ITLS-PIES', 'SDIT-CSD-ITLS-VLEO', 'SDIT-CSD-OBO-RDE', 'SDIT-DATA-ASSETS',
+ 'SDIT-DATAASSETS-MONGODBA', 'SDIT-DATAASSETS-ORACLEDBA', 'SDIT-EDIS-CDP-DAAS-DATALOGISTICS',
+ 'SDIT-EDIS-CDP-DAAS-NDS', 'SDIT-EDIS-CDS-DIGITALSERVICES', 'SDIT-EDIS-CIC-NEBULA',
+ 'SDIT-EDIS-ITEI-CLOUD', 'SDIT-EDIS-ITEI-EMAIL', 'SDIT-EDIS-ITEI-TAAS-CIE',
+ 'SDIT-EDIS-ITEI-TAAS-DM', 'SDIT-EDIS-NAAS-DESIGN', 'SDIT-EDIS-NAAS-FIREWALL',
+ 'SDIT-EDIS-NAAS-IMPLEMENTATION', 'SDIT-EDIS-NAAS-INSTALL', 'SDIT-EDIS-NAAS-NAT',
+ 'SDIT-EDIS-NAAS-OPERATIONS', 'SDIT-EDIS-PAAS-PRIVATE-CLOUD', 'SDIT-EDIS-PAAS-PUBCLD',
+ 'SDIT-ITSA-OPS', 'SDIT-ITSA-OPS-IDOS', 'SDIT-ITSA-OPS-PQR-SCI', 'SDIT-ITSA-TOO',
+ 'SDIT-MOBILE', 'SDIT-MOBILE-ACTIVATION', 'SDIT-PCDS-SA-ACTIVATIONS',
+ 'SDIT-PCDS-SA-ANALYSTS', 'SDIT-PCDS-SA-PROVISIONING', 'SDIT-PCDS-SA-SCI',
+ 'SDIT-SVCEXP-VCT-DESIGN', 'SN-OPS-NEWS', 'SPECTRUM ENTERPRISE', 'SPECTRUM REACH',
+ 'SROPS-DATA', 'SROPS-SPECTRUM REACH OPS', 'TEST-NEW-TEAM', 'TEST-OLD-TEAM',
+ 'VDE-MAPD-DEV', 'VDE-MAPD-DEV2', 'VDE-VOD-NVIS', 'VDE-VOD-REPORTING',
+ 'WTG-WAE-ACCESS ENGINEERING', 'WTG-WCE-SYS ENG', 'WTG-WDE-DEVICE ENGINEERING',
+ 'WTG-WRD-CONNECTIVITY', 'WTG-WRD-RESEARCH AND DEVELOPMENT',
+];
+
+export const EQUIP_STATUSES = ['ACTIVE', 'DESIGNED', 'PENDING DECOMMISSION', 'DECOMMISSIONED'];
+
+export const EQUIPMENT_CLASSES = ['S', 'C'];
+
+// Small list — columns that should render as searchable dropdowns
+// Maps column ID → options array
+export const COLUMN_PICKLISTS = {
+ RESPONSIBLE_TEAM: RESPONSIBLE_TEAMS,
+ EQUIP_STATUS: EQUIP_STATUSES,
+ EQUIPMENT_CLASS: EQUIPMENT_CLASSES,
+};