ChemPal Documentation - v1.7.0
    Preparing search index...
    • Enhanced ResultsTable component using chem-pal styling with local functionality

      Features:

      • Modern table styling from chem-pal
      • Local search functionality and streaming results
      • Context menu for product rows
      • Auto column sizing
      • Pagination and filtering
      • Drawer system integration

      Parameters

      Returns ReactElement

      export default function ResultsTable({
      getRowCanExpand,
      columnFilterFns,
      }: ResultsTableProps): ReactElement {
      const appContext = useAppContext();
      // Count of active advanced-search (drawer) filters — blue-highlights the flask
      // button and is surfaced on hover. 0 when the context isn't ready or all default.
      const advancedFilterCount = appContext
      ? countActiveSearchFilters({
      selectedSuppliers: appContext.selectedSuppliers,
      searchFilters: appContext.searchFilters,
      userSettings: appContext.userSettings,
      })
      : 0;
      // How many columns have an active filter (each entry is one column) —
      // independent of whether the filter row is shown. Drives the filter-toggle
      // icon's blue highlight and its hover count.
      const activeColumnFilterCount = columnFilterFns[0].length;
      const hasActiveColumnFilters = activeColumnFilterCount > 0;
      const [showFilters, setShowFilters] = useState(false);
      const [columnMenuAnchor, setColumnMenuAnchor] = useState<null | HTMLElement>(null);
      const [globalFilter, setGlobalFilter] = useState('');
      const globalFilterInputRef = useRef<HTMLInputElement>(null);
      const scrollContainerRef = useRef<HTMLDivElement>(null);

      // Bridge hotkeys (fired from App.tsx) into local state/refs.
      useEffect(() => {
      const onFocus = () => {
      const el = globalFilterInputRef.current;
      if (!el) return;
      el.focus();
      el.select();
      };
      const onToggle = () => setShowFilters((v) => !v);
      window.addEventListener(HotkeyEvent.FOCUS_GLOBAL_FILTER, onFocus);
      window.addEventListener(HotkeyEvent.TOGGLE_COLUMN_FILTERS, onToggle);
      return () => {
      window.removeEventListener(HotkeyEvent.FOCUS_GLOBAL_FILTER, onFocus);
      window.removeEventListener(HotkeyEvent.TOGGLE_COLUMN_FILTERS, onToggle);
      };
      }, []);

      // Enhanced search hook that maintains streaming behavior
      const {
      searchResults,
      isLoading,
      isAborting,
      error,
      executeSearch,
      handleStopSearch,
      excludeProduct,
      tableText,
      executedQuery,
      } = useSearch();

      // Watch for pending search queries triggered from HistoryPanel or the drawer.
      // The ref guard dedupes against:
      // 1. StrictMode double-invoke of effects on mount (dev only)
      // 2. The `appContext` dep in the array — context object identity changes on
      // every context state update, so this effect re-fires whenever anything
      // in AppContext changes, not just `pendingSearchQuery`.
      // 3. `executeSearch` / `appContext` not being stable references, which
      // would otherwise cause spurious re-runs.
      // Without the guard, a single submitted query fires `executeSearch` twice
      // (and two sets of supplier HTTP requests).
      const lastHandledPendingQueryRef = useRef<string | null>(null);
      useEffect(() => {
      const pending = appContext?.pendingSearchQuery;
      if (!pending) {
      // Reset so the same query can be submitted again later.
      lastHandledPendingQueryRef.current = null;
      return;
      }
      if (lastHandledPendingQueryRef.current === pending) return;
      lastHandledPendingQueryRef.current = pending;
      executeSearch(pending);
      appContext?.setPendingSearchQuery(null);
      }, [appContext?.pendingSearchQuery, executeSearch, appContext]);

      // Context menu functionality
      const { contextMenu, handleContextMenu, handleCloseContextMenu } = useContextMenu();

      const table = useResultsTable({
      showSearchResults: searchResults,
      columnFilterFns,
      globalFilterFns: [globalFilter, setGlobalFilter],
      getRowCanExpand,
      userSettings: appContext?.userSettings ?? DEFAULT_USER_SETTINGS,
      });

      // ── Table state persistence ──────────────────────────────────────────
      // Uses the TanStack "fully controlled state" pattern: take over the
      // table's state via `table.setOptions`, persist the slices we care about
      // (sorting, pagination, expanded rows, column visibility) to
      // chrome.storage.session, and restore them on mount.
      const [tableState, setTableState] = useState<TableState>(table.initialState);
      const isStateLoadedRef = useRef(false);

      // The rows-per-page the user explicitly asked for, via the Select or the
      // "show all" hotkey. Filtering can clamp the *effective* pageSize below this
      // when fewer rows are available; tracking the intent lets us restore it once
      // the row count grows back (e.g. the user clears a column filter).
      // `Number.POSITIVE_INFINITY` means "all rows". Declared here (above the load
      // effect) so a restored pageSize can hydrate it — otherwise the reconciliation
      // below would immediately reset a persisted non-default row limit to the default.
      const desiredPageSizeRef = useRef(tableState.pagination?.pageSize ?? 10);

      // Collapse all variant rows whenever a new search begins. Without this,
      // the persisted `expanded` state from the previous search can leak into
      // the new results when row IDs happen to collide (e.g. both searches
      // include a product at the same index).
      const prevIsLoadingRef = useRef(false);
      useEffect(() => {
      if (isLoading && !prevIsLoadingRef.current) {
      setTableState((prev) => ({ ...prev, expanded: {} }));
      }
      prevIsLoadingRef.current = isLoading;
      }, [isLoading]);

      // When a search finishes, auto-hide hideable columns that have no data in any
      // row (across all results and their variants, not just the visible page), and
      // restore any column we previously auto-hid once it has data again. Only the
      // columns we hid are touched, so manual visibility choices and default-hidden
      // columns are left intact. Gated by the `autoHideEmptyColumns` setting (default
      // on); when off, no columns are auto-hidden and any we previously hid are
      // restored so the toggle takes effect immediately.
      const autoHideEmptyColumns = appContext?.userSettings?.autoHideEmptyColumns ?? true;
      const autoHiddenColumnsRef = useRef<Set<string>>(new Set());
      const prevIsLoadingForAutoHideRef = useRef(false);
      useEffect(() => {
      const searchJustFinished = prevIsLoadingForAutoHideRef.current && !isLoading;
      prevIsLoadingForAutoHideRef.current = isLoading;

      // Feature disabled: restore anything we auto-hid, then do nothing further.
      if (!autoHideEmptyColumns) {
      if (autoHiddenColumnsRef.current.size === 0) return;
      const defaultVisibility = table.initialState.columnVisibility ?? {};
      setTableState((prev) => {
      const columnVisibility = { ...prev.columnVisibility };
      for (const id of autoHiddenColumnsRef.current) {
      const defaultVisible = defaultVisibility[id];
      if (defaultVisible === undefined) delete columnVisibility[id];
      else columnVisibility[id] = defaultVisible;
      }
      return { ...prev, columnVisibility };
      });
      autoHiddenColumnsRef.current = new Set();
      return;
      }

      if (!searchJustFinished || searchResults.length === 0) return;

      const emptyColumnIds = new Set(getEmptyHideableColumnIds(table));
      const defaultVisibility = table.initialState.columnVisibility ?? {};

      setTableState((prev) => {
      const columnVisibility = { ...prev.columnVisibility };
      // Restore columns we previously auto-hid that now have data, back to their
      // default visibility (keeps default-hidden columns like `availability` hidden).
      for (const id of autoHiddenColumnsRef.current) {
      if (emptyColumnIds.has(id)) continue;
      const defaultVisible = defaultVisibility[id];
      if (defaultVisible === undefined) delete columnVisibility[id];
      else columnVisibility[id] = defaultVisible;
      }
      // Hide columns that have no data in this result set.
      for (const id of emptyColumnIds) columnVisibility[id] = false;
      return { ...prev, columnVisibility };
      });
      autoHiddenColumnsRef.current = emptyColumnIds;
      // `table` is a stable instance from useResultsTable; recompute is driven by
      // the search lifecycle (isLoading), the resulting data, and the toggle.
      }, [isLoading, searchResults, autoHideEmptyColumns]);

      // Load persisted state once on mount
      useEffect(() => {
      const load = async () => {
      try {
      const data = await cstorage.session.get([CACHE.TABLE_STATE]);
      const stored = data[CACHE.TABLE_STATE] as
      | (Partial<TableState> & { globalFilter?: string; showFilters?: boolean })
      | undefined;
      if (stored && typeof stored === 'object') {
      if (typeof stored.globalFilter === 'string') {
      setGlobalFilter(stored.globalFilter);
      }
      if (typeof stored.showFilters === 'boolean') {
      setShowFilters(stored.showFilters);
      }
      if (Array.isArray(stored.columnFilters)) {
      columnFilterFns[1](stored.columnFilters);
      }
      // Hydrate the "desired rows-per-page" intent from the restored pageSize
      // so the reconciliation below keeps the persisted row limit instead of
      // resetting it to the default once cached results repopulate the table.
      if (typeof stored.pagination?.pageSize === 'number') {
      desiredPageSizeRef.current = stored.pagination.pageSize;
      }
      setTableState((prev) => ({ ...prev, ...stored }));
      }
      } catch (error) {
      console.warn('Failed to load table state from session storage:', { error });
      }
      isStateLoadedRef.current = true;
      };
      load();
      }, []);

      // Override state management — the table reads state from our local
      // `tableState` and pushes every change back through `setTableState`.
      // Column filters and global filter remain externally controlled.
      table.setOptions((prev) => ({
      ...prev,
      state: {
      ...tableState,
      columnFilters: columnFilterFns[0],
      globalFilter,
      },
      onStateChange: setTableState,
      }));

      // Debounced save — only persist the slices we care about restoring

      const saveTableState = useCallback(
      debounce(async (s: TableState & { showFilters?: boolean }) => {
      try {
      await cstorage.session.set({
      [CACHE.TABLE_STATE]: {
      sorting: s.sorting,
      pagination: s.pagination,
      expanded: s.expanded,
      columnVisibility: s.columnVisibility,
      columnFilters: s.columnFilters,
      globalFilter: s.globalFilter,
      showFilters: s.showFilters,
      },
      });
      } catch (error) {
      console.warn('Failed to persist table state:', { error });
      }
      }, 300),
      [],
      );

      // Persist whenever controlled state changes (skip until initial load
      // completes to avoid overwriting stored state with defaults).
      // globalFilter, columnFilters, and showFilters are managed externally
      // but still persisted alongside table state.
      useEffect(() => {
      if (!isStateLoadedRef.current) return;
      saveTableState({
      ...tableState,
      globalFilter,
      columnFilters: columnFilterFns[0],
      showFilters,
      });
      }, [tableState, globalFilter, columnFilterFns[0], showFilters, saveTableState]);

      // Clamp pageSize synchronously so the MUI Select never renders with an
      // out-of-range value (e.g. persisted pageSize=36 when valid options are
      // [10, 20, 40, 74]). Must happen during render, not in a useEffect,
      // because MUI warns before effects run.
      const filteredRowCount = table.getRowModel().rows.filter((row) => row.depth === 0).length;
      const totalRowCount = table.getFilteredRowModel().rows.length;
      const supplierResultsCount = table.getColumn('supplier')?.getFacetedUniqueValues().size ?? 0;

      // Emit the filtered row count; the badge controller owns the badge update.
      // Suppress leading zeros (the empty table before results populate on open)
      // so they don't clear a badge App already set to the restored count.
      const hasHadResultsRef = useRef(false);
      useEffect(() => {
      if (totalRowCount > 0) hasHadResultsRef.current = true;
      if (totalRowCount === 0 && !hasHadResultsRef.current) return;
      emitSearchEvent(SearchEvent.RESULTS_COUNT, { count: totalRowCount });
      }, [filteredRowCount, totalRowCount]);

      // Bridge the results-table hotkeys (fired from App.tsx) into the table API.
      // `table` is a stable instance, so row counts are read fresh at event time.
      const setColumnFilters = columnFilterFns[1];
      useEffect(() => {
      const onExpandAll = () => {
      // Expand only rows that can actually expand — setting the global
      // `expanded: true` flag would render an empty detail panel for every
      // non-expandable row, since the row render only guards on getIsExpanded().
      const expanded: Record<string, boolean> = {};
      for (const row of table.getRowModel().rows) {
      if (row.getCanExpand()) expanded[row.id] = true;
      }
      table.setExpanded(expanded);
      };
      const onCollapseAll = () => table.setExpanded({});
      const onScrollToTop = () => scrollContainerRef.current?.scrollTo({ top: 0 });
      const onShowAll = () => {
      const rowCount = table.getFilteredRowModel().rows.length;
      if (rowCount > 0) {
      desiredPageSizeRef.current = Number.POSITIVE_INFINITY;
      table.setPageSize(rowCount);
      }
      };
      const onClearColumnFilters = () => setColumnFilters([]);
      window.addEventListener(HotkeyEvent.EXPAND_ALL_ROWS, onExpandAll);
      window.addEventListener(HotkeyEvent.COLLAPSE_ALL_ROWS, onCollapseAll);
      window.addEventListener(HotkeyEvent.SCROLL_RESULTS_TO_TOP, onScrollToTop);
      window.addEventListener(HotkeyEvent.SHOW_ALL_ROWS, onShowAll);
      window.addEventListener(HotkeyEvent.CLEAR_COLUMN_FILTERS, onClearColumnFilters);
      return () => {
      window.removeEventListener(HotkeyEvent.EXPAND_ALL_ROWS, onExpandAll);
      window.removeEventListener(HotkeyEvent.COLLAPSE_ALL_ROWS, onCollapseAll);
      window.removeEventListener(HotkeyEvent.SCROLL_RESULTS_TO_TOP, onScrollToTop);
      window.removeEventListener(HotkeyEvent.SHOW_ALL_ROWS, onShowAll);
      window.removeEventListener(HotkeyEvent.CLEAR_COLUMN_FILTERS, onClearColumnFilters);
      };
      }, [table, setColumnFilters]);

      // Reconcile the effective pageSize with what the user actually asked for
      // (`desiredPageSizeRef`) whenever the valid options change. Filtering shrinks
      // the row count, which can clamp the effective size below the user's choice;
      // clearing the filter grows it back. Reconciling against the *intent* rather
      // than the current (possibly-clamped) size means clearing a filter restores
      // the user's original selection — e.g. "All" survives a filter round-trip
      // instead of collapsing to the default 10.
      //
      // Valid sizes come from the *filter-applied total* (totalRowCount), not the
      // page-visible count (filteredRowCount): the latter creates a self-reinforcing
      // collapse where valid sizes shrink to `[page-visible]`, forcing pageSize down,
      // which keeps only that many rows visible, etc. The options Select below uses
      // the total as well. Runs during render, not in an effect, because the MUI
      // Select warns about an out-of-range value before effects run.
      if (totalRowCount > 0) {
      const validSizes = generatePageSizes(totalRowCount, 10, 5);
      const currentPageSize = tableState.pagination?.pageSize ?? 10;
      const desired = desiredPageSizeRef.current;
      const target =
      desired >= totalRowCount
      ? totalRowCount // "all", or more than currently available → show every row
      : validSizes.includes(desired)
      ? desired
      : (validSizes.filter((size) => size <= desired).pop() ?? validSizes[0]);
      if (currentPageSize !== target) {
      table.setPageSize(target);
      }
      }

      // Initialize column visibility - this effect is still needed
      useEffect(() => {
      if (appContext && !isEmpty(appContext.userSettings.hideColumns)) {
      table.getAllLeafColumns().map((column: Column<Product>) => {
      if (appContext.userSettings?.hideColumns?.includes(column.id)) {
      column.toggleVisibility(false);
      }
      });
      }
      }, [appContext?.userSettings.hideColumns, table]);

      // Auto column sizing is driven by the raw searchResults (not the filtered
      // row model) so that filter input keystrokes don't trigger column remeasuring.
      const { getMeasurementTableProps, autoSizeColumns } = useAutoColumnSizing(table, searchResults);

      // A column-resize drag ends with a stray `click` that bubbles to the header's
      // sort toggle (the click's target resolves to the <th>, so stopping propagation
      // on the resizer alone doesn't catch it). Mark a resize in progress on the
      // handle's pointer-down and clear it one frame after pointer-up — after the
      // stray click has already fired and been swallowed by the header handler below.
      const suppressSortAfterResizeRef = useRef(false);
      const markResizeStart = () => {
      suppressSortAfterResizeRef.current = true;
      window.addEventListener(
      'pointerup',
      () =>
      requestAnimationFrame(() => {
      suppressSortAfterResizeRef.current = false;
      }),
      { once: true },
      );
      };

      const handleSearch = (query: string) => {
      if (query.trim()) {
      executeSearch(query.trim());
      }
      };

      const handleKeyPress = (event: KeyboardEvent) => {
      if (event.key === 'Enter' && isInputElement(event.target)) {
      handleSearch(event.target.value);
      }
      };

      const toggleFilters = () => {
      setShowFilters(!showFilters);
      };

      // Clear the results table's column filters (and the global filter) so hidden
      // results become visible again.
      const clearColumnFilters = () => {
      setColumnFilters([]);
      setGlobalFilter('');
      };

      // Reset every drawer search filter to its default and re-run the last query.
      // Re-triggers via the pending-query path so the search reads the freshly
      // cleared filters on the next render (rather than a stale closure).
      const retrySearchWithoutFilters = () => {
      if (!appContext) return;
      appContext.setSelectedSuppliers([]);
      appContext.setSearchFilters({
      ...appContext.searchFilters,
      availability: [],
      country: [],
      shippingType: [],
      });
      appContext.setUserSettings({
      ...appContext.userSettings,
      priceMin: undefined,
      priceMax: undefined,
      });
      if (executedQuery) appContext.setPendingSearchQuery(executedQuery);
      };

      // Content for the empty table body — distinguishes "search returned nothing
      // while drawer filters were active" (offer a filter-free retry) from "results
      // exist but the table's column filters hide them all" (offer to clear those).
      const renderEmptyState = (): ReactNode => {
      if (searchResults.length === 0) {
      if (isLoading) return i18n('results_status_searching');
      if (executedQuery && advancedFilterCount > 0) {
      return (
      <>
      {i18n('results_status_no_results_filtered')}
      <Link
      component="button"
      type="button"
      onClick={retrySearchWithoutFilters}
      sx={{ display: 'block', mt: 1, mx: 'auto', cursor: 'pointer' }}
      >
      {i18n('results_retry_without_filters')}
      </Link>
      </>
      );
      }
      return tableText || i18n('results_status_no_search_query');
      }
      // searchResults exist but none are shown → the column/global filters hid them.
      const columnFiltersActive =
      table.getState().columnFilters.length > 0 || Boolean(table.getState().globalFilter);
      if (columnFiltersActive) {
      return (
      <>
      {i18n('results_status_hidden_by_column_filters', [String(searchResults.length)])}{' '}
      <Link component="button" type="button" onClick={clearColumnFilters}>
      {i18n('results_clear_column_filters')}
      </Link>
      </>
      );
      }
      return i18n('results_status_no_results_found');
      };

      return (
      <>
      <LoadingBackdrop
      open={isLoading}
      // Count top-level rows only — `searchResults.length` can double-count
      // when suppliers yield variants as flat products rather than nested.
      // `getFilteredRowModel().rows.length` is the committed parent-row count
      // (sub-rows live on each row's `.subRows`), which is what users see.
      resultCount={totalRowCount}
      supplierResultsCount={supplierResultsCount}
      isAborting={isAborting}
      onClick={handleStopSearch}
      />
      <div className={resultStyles['results-container']}>
      <div className={resultStyles['results-header']}>
      <div className={resultStyles['header-left']}>
      {appContext?.setPanel && (
      <BackButton
      onClick={() => appContext.setPanel!(0)}
      size="small"
      aria-label={i18n('common_back_to_search')}
      >
      <ArrowBackIcon />
      </BackButton>
      )}
      </div>
      <div className={resultStyles['header-right']}>
      {/* Advanced search: opens the drawer's Search tab (mirrors the home
      page's ScienceIcon). First icon, to the left of the others. Turns
      blue with a filter count on hover when any drawer filter is set. */}
      <Tooltip
      title={
      advancedFilterCount > 0
      ? i18n('search_active_filters', [String(advancedFilterCount)])
      : i18n('search_advanced_options')
      }
      >
      <ColoredIconButton
      onClick={() => appContext?.toggleDrawer(DRAWER_INDEX.SEARCH)}
      size="small"
      iconColor={advancedFilterCount > 0 ? '#4e73af' : '#666'}
      aria-label={i18n('search_advanced_options')}
      >
      <ScienceIcon />
      </ColoredIconButton>
      </Tooltip>
      <Tooltip
      title={
      hasActiveColumnFilters
      ? i18n('results_active_column_filters', [String(activeColumnFilterCount)])
      : i18n('results_toggle_filters')
      }
      >
      <FilterIconButton
      onClick={toggleFilters}
      size="small"
      // Blue when a column filter is applied, or while the filter row is
      // open (preserving the toggle's pressed-state affordance).
      isActive={hasActiveColumnFilters || showFilters}
      activeColor="#4e73af"
      textColor="#666"
      aria-label={i18n('results_toggle_filters')}
      >
      <FilterListIcon />
      </FilterIconButton>
      </Tooltip>
      <ColoredIconButton
      onClick={(e) => setColumnMenuAnchor(e.currentTarget)}
      size="small"
      iconColor="#666"
      aria-label={i18n('results_column_visibility')}
      >
      <ViewColumnIcon />
      </ColoredIconButton>
      <ColoredIconButton
      onClick={() => appContext?.toggleDrawer(DRAWER_INDEX.SETTINGS)}
      size="small"
      iconColor="#666"
      aria-label={i18n('results_open_options')}
      >
      <SettingsIcon />
      </ColoredIconButton>
      {/* Maximize: open in a full tab. Last icon, popup/side-panel only. */}
      {!isTabView() && (
      <ColoredIconButton
      onClick={() => void openExtensionTab()}
      size="small"
      iconColor="#666"
      aria-label={i18n('common_open_in_tab')}
      >
      <OpenInNewIcon />
      </ColoredIconButton>
      )}
      </div>
      </div>

      {/* <div className="results-title">Search Results ({searchResults.length} found)</div> */}

      <ResultsHeaderContainer>
      {/* Show the originating query here; the result count lives in the
      pagination footer below. */}
      <ResultsCountDisplay
      title={executedQuery ? i18n('results_searched_for', [executedQuery]) : undefined}
      >
      {executedQuery ? i18n('results_query', [executedQuery]) : ''}
      </ResultsCountDisplay>
      {/* Only show the global filter if there are results. Based on
      searchResults (not the filtered row model) so the input doesn't
      vanish once the user's filter query matches zero rows. */}
      {searchResults.length > 0 && (
      <GlobalFilterTextField
      size="small"
      variant="outlined"
      placeholder={i18n('results_filter_placeholder')}
      value={globalFilter}
      onChange={(e) => setGlobalFilter(e.target.value)}
      inputRef={globalFilterInputRef}
      slotProps={{
      input: {
      onKeyDown: handleKeyPress,
      'aria-label': i18n('results_filter_aria'),
      },
      }}
      />
      )}
      </ResultsHeaderContainer>

      <Box
      ref={scrollContainerRef}
      className={`${resultStyles['results-paper']} ${resultStyles['results-paper-container']}`}
      >
      {/* Hidden measurement table for auto-sizing */}
      <table
      className={resultStyles['hidden-measurement-table']}
      {...getMeasurementTableProps()}
      >
      <thead className="results-table-column-headers">
      <tr>
      {table.getFlatHeaders().map((header) => (
      <th key={header.id}>
      {/* Wrap the header in a nowrap span so its own text width is
      measurable independently of the column width. Rendered
      via flexRender (not col.id) so function headers like
      "Price (USD)" are measured correctly and set the column's
      minimum width. */}
      <span style={{ whiteSpace: 'nowrap' }}>
      {flexRender(header.column.columnDef.header, header.getContext())}
      </span>
      </th>
      ))}
      </tr>
      </thead>
      <tbody className="results-table-body">
      {table
      .getRowModel()
      .rows.slice(0, 5)
      .map((row) => (
      <tr key={row.id}>
      {row.getVisibleCells().map((cell) => (
      // `nowrap` (no width pin) makes scrollWidth report the
      // natural single-line content width, so each column fits
      // its content (clamped by meta.autoSizeMax) instead of a
      // uniform width that leaves dead space after short values.
      <td key={cell.id} style={{ whiteSpace: 'nowrap' }}>
      {typeof cell.column.columnDef.cell === 'function'
      ? cell.column.columnDef.cell(cell.getContext())
      : ''}
      </td>
      ))}
      </tr>
      ))}
      </tbody>
      </table>

      <SearchResultsTable>
      {/* Table Head */}
      <StyledTableHead>
      {table.getHeaderGroups().map((headerGroup) => (
      <TableRow key={headerGroup.id}>
      {headerGroup.headers.map((header) => (
      <StickyHeaderCell
      key={header.id}
      canSort={header.column.getCanSort()}
      cellWidth={header.getSize()}
      onClick={(event) => {
      // Swallow the stray click that ends a column resize so it
      // doesn't toggle the sort (see markResizeStart).
      if (suppressSortAfterResizeRef.current) return;
      header.column.getToggleSortingHandler()?.(event);
      }}
      style={header.column.columnDef.meta?.style}
      >
      {header.isPlaceholder ? null : (
      <HeaderCellContent>
      {flexRender(header.column.columnDef.header, header.getContext())}
      {header.column.getCanSort() && (
      <SortIndicator>
      {
      {
      asc: <ArrowDropUpIcon />,
      desc: <ArrowDropDownIcon />,
      }[String(header.column.getIsSorted())]
      }
      </SortIndicator>
      )}
      </HeaderCellContent>
      )}
      {header.column.getCanResize() && (
      <ColumnResizer
      isResizing={header.column.getIsResizing()}
      onMouseDown={(event) => {
      markResizeStart();
      header.getResizeHandler()(event);
      }}
      onTouchStart={(event) => {
      markResizeStart();
      header.getResizeHandler()(event);
      }}
      // Stop the drag/click from bubbling to the header's
      // sort toggle handler.
      onClick={(event) => event.stopPropagation()}
      // Double-click any handle to best-fit every column to
      // its content width.
      onDoubleClick={(event) => {
      event.stopPropagation();
      autoSizeColumns();
      }}
      className={`resizer${header.column.getIsResizing() ? ' isResizing' : ''}`}
      />
      )}
      </StickyHeaderCell>
      ))}
      </TableRow>
      ))}

      {/* Filter Row */}
      {showFilters &&
      table.getHeaderGroups().map((headerGroup) => {
      return (
      <TableRow key={`${headerGroup.id}-filters`}>
      {headerGroup.headers.map((header) => {
      if (header.column.id === 'expander') {
      return (
      <FilterTableCell
      key={`${header.id}-filter`}
      cellWidth={27.5}
      sx={{ flexShrink: 0 }}
      >
      <Tooltip title={i18n('results_clear_filters')}>
      <IconButton
      size="small"
      onClick={() => columnFilterFns[1]([])}
      aria-label={i18n('results_clear_filters')}
      sx={{ flexShrink: 0 }}
      >
      <SearchOffIcon fontSize="small" sx={{ flexShrink: 0 }} />
      </IconButton>
      </Tooltip>
      </FilterTableCell>
      );
      }
      return (
      <FilterTableCell key={`${header.id}-filter`} cellWidth={header.getSize()}>
      {header.column.getCanFilter() ? (
      <FilterVariantCell header={header} />
      ) : null}
      </FilterTableCell>
      );
      })}
      </TableRow>
      );
      })}
      </StyledTableHead>

      {/* Table Body */}
      <StyledTableBody>
      {table.getRowModel().rows.length > 0 ? (
      // Render only top-level rows. Variants still exist as sub-rows in
      // the model (so hierarchy filtering works), but instead of
      // rendering them as their own rows we surface them — plus the
      // product image and detail fields — in an expanded panel below.
      table
      .getRowModel()
      .rows.filter((row) => row.depth === 0)
      .map((row) => (
      <Fragment key={row.id}>
      <SubRowTableRow
      isSubRow={false}
      onContextMenu={(e) => handleContextMenu(e, row.original)}
      >
      {row.getVisibleCells().map((cell) => (
      <StyledTableCell
      key={cell.id}
      className={resultStyles['styled-table-cell']}
      style={{
      textAlign: cell.column.columnDef.meta?.style?.textAlign,
      // Under table-layout: fixed, let long unbreakable
      // tokens wrap instead of overflowing the column.
      overflowWrap: 'anywhere',
      }}
      >
      {cell.column.columnDef.meta?.truncate ? (
      // The fixed-layout cell already has a definite
      // width, so the block fills it and ellipsizes the
      // overflow; full text shows in the hover tooltip.
      <TruncatedCellText title={String(cell.getValue() ?? '')}>
      {flexRender(cell.column.columnDef.cell, cell.getContext())}
      </TruncatedCellText>
      ) : (
      flexRender(cell.column.columnDef.cell, cell.getContext())
      )}
      </StyledTableCell>
      ))}
      </SubRowTableRow>
      {row.getIsExpanded() && (
      <TableRow>
      <ProductDetailPanelCell colSpan={row.getVisibleCells().length}>
      <ProductDetailPanel row={row} table={table} />
      </ProductDetailPanelCell>
      </TableRow>
      )}
      </Fragment>
      ))
      ) : (
      <TableRow className={resultStyles['styled-table-row']}>
      <EmptyStateCell colSpan={table.getAllColumns().length}>
      {renderEmptyState()}
      </EmptyStateCell>
      </TableRow>
      )}
      </StyledTableBody>
      </SearchResultsTable>

      {/* Enhanced error handling */}
      {error && (
      <ErrorContainer className={resultStyles['error-container']}>
      <p>{i18n('results_error', [error])}</p>
      <ErrorRetryButton
      onClick={() => window.location.reload()}
      className={resultStyles['error-retry-button']}
      >
      {i18n('results_retry')}
      </ErrorRetryButton>
      </ErrorContainer>
      )}

      {/* Pagination Controls - Only show if more than 1 page */}
      {totalRowCount > 10 && (
      <PaginationContainer>
      {/* Page Size Selector */}
      <PageSizeContainer>
      <Typography variant="body2">{i18n('results_show')}:</Typography>
      <FormControl size="small">
      <PageSizeSelect
      value={table.getState().pagination.pageSize}
      onChange={(e) => {
      const next = Number(e.target.value);
      // "All" is the option whose value equals the current total;
      // record it as an unbounded intent so it survives a filter
      // round-trip even as the total changes.
      desiredPageSizeRef.current =
      next >= totalRowCount ? Number.POSITIVE_INFINITY : next;
      table.setPageSize(next);
      }}
      aria-label={i18n('results_rows_per_page_aria')}
      >
      {generatePageSizes(totalRowCount, 10, 5).map((pageSize) => (
      <MenuItem key={pageSize} value={pageSize}>
      {pageSize === totalRowCount ? i18n('results_all') : pageSize}
      </MenuItem>
      ))}
      </PageSizeSelect>
      </FormControl>
      <Typography variant="body2">{i18n('results_rows')}</Typography>
      </PageSizeContainer>

      {/* Page Info — "Showing N of M" surfaces the post-filter vs
      pre-filter delta when the user narrows the results with a
      column / global filter. When no filter is active (filtered
      === total) it collapses back to the plain total form. */}
      <Typography variant="body2">
      {i18n('results_page_of_total', [
      String(table.getState().pagination.pageIndex + 1),
      String(table.getPageCount()),
      ]) + ' '}
      {filteredRowCount === totalRowCount
      ? i18n('results_total', [String(totalRowCount)])
      : i18n('results_showing', [String(filteredRowCount), String(totalRowCount)])}
      </Typography>

      {/* Navigation Buttons */}
      <NavigationContainer>
      <IconButton
      onClick={() => table.setPageIndex(0)}
      disabled={!table.getCanPreviousPage()}
      size="small"
      >
      <FirstPageIcon />
      </IconButton>
      <IconButton
      onClick={() => table.previousPage()}
      disabled={!table.getCanPreviousPage()}
      size="small"
      >
      <ChevronLeftIcon />
      </IconButton>
      <IconButton
      onClick={() => table.nextPage()}
      disabled={!table.getCanNextPage()}
      size="small"
      >
      <ChevronRightIcon />
      </IconButton>
      <IconButton
      onClick={() => table.setPageIndex(table.getPageCount() - 1)}
      disabled={!table.getCanNextPage()}
      size="small"
      >
      <LastPageIcon />
      </IconButton>
      </NavigationContainer>
      </PaginationContainer>
      )}
      </Box>

      {/* Column Visibility Menu */}
      <Menu
      anchorEl={columnMenuAnchor}
      open={Boolean(columnMenuAnchor)}
      onClose={() => setColumnMenuAnchor(null)}
      className={styles['column-visibility-menu']}
      >
      {table
      .getAllLeafColumns()
      .filter((column) => column.getCanHide())
      .map((column) => {
      // Read the header off the column definition (present for hidden
      // columns too, unlike getFlatHeaders which only covers visible ones).
      // String headers are already localized; the price column's function
      // header falls back to its plain i18n label so it isn't stringified
      // into JS source or dropped to the raw lowercase column id.
      const headerDef = column.columnDef.header;
      const label = typeof headerDef === 'string' ? headerDef : i18n(`column_${column.id}`);
      return (
      <ColumnMenuItemContainer key={column.id}>
      <FormControlLabel
      control={
      <Checkbox
      checked={column.getIsVisible()}
      onChange={column.getToggleVisibilityHandler()}
      />
      }
      label={<ListItemText primary={label} />}
      />
      </ColumnMenuItemContainer>
      );
      })}
      </Menu>

      {/* Context Menu */}
      {contextMenu && contextMenu.product && (
      <ContextMenu
      x={contextMenu.x}
      y={contextMenu.y}
      product={contextMenu.product}
      table={table}
      onClose={handleCloseContextMenu}
      onExcludeProduct={excludeProduct}
      executedQuery={executedQuery}
      />
      )}
      </div>
      </>
      );
      }