@xcelsior/ui-spreadsheets 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-spreadsheets",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -1614,10 +1614,123 @@ interface StockData {
1614
1614
  profit: number;
1615
1615
  }
1616
1616
 
1617
- const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META', 'TSLA', 'NVDA', 'JPM', 'V', 'JNJ', 'WMT', 'PG', 'MA', 'UNH', 'HD', 'DIS', 'BAC', 'XOM', 'PFE', 'KO', 'PEP', 'CSCO', 'ADBE', 'NFLX', 'INTC', 'CRM', 'ABT', 'NKE', 'MRK', 'TMO', 'ORCL', 'ACN', 'MDT', 'COST', 'LLY', 'AVGO', 'TXN', 'NEE', 'UNP', 'DHR', 'QCOM', 'PM', 'BMY', 'RTX', 'HON', 'UPS', 'LOW', 'AMGN', 'SBUX', 'IBM'];
1618
- const companies = ['Apple Inc.', 'Microsoft Corp.', 'Alphabet Inc.', 'Amazon.com Inc.', 'Meta Platforms', 'Tesla Inc.', 'NVIDIA Corp.', 'JPMorgan Chase', 'Visa Inc.', 'Johnson & Johnson', 'Walmart Inc.', 'Procter & Gamble', 'Mastercard', 'UnitedHealth', 'Home Depot', 'Walt Disney', 'Bank of America', 'Exxon Mobil', 'Pfizer Inc.', 'Coca-Cola', 'PepsiCo Inc.', 'Cisco Systems', 'Adobe Inc.', 'Netflix Inc.', 'Intel Corp.', 'Salesforce', 'Abbott Labs', 'Nike Inc.', 'Merck & Co.', 'Thermo Fisher', 'Oracle Corp.', 'Accenture', 'Medtronic', 'Costco', 'Eli Lilly', 'Broadcom', 'Texas Instruments', 'NextEra Energy', 'Union Pacific', 'Danaher', 'Qualcomm', 'Philip Morris', 'Bristol-Myers', 'Raytheon', 'Honeywell', 'UPS', 'Lowes', 'Amgen', 'Starbucks', 'IBM'];
1617
+ const tickers = [
1618
+ 'AAPL',
1619
+ 'MSFT',
1620
+ 'GOOGL',
1621
+ 'AMZN',
1622
+ 'META',
1623
+ 'TSLA',
1624
+ 'NVDA',
1625
+ 'JPM',
1626
+ 'V',
1627
+ 'JNJ',
1628
+ 'WMT',
1629
+ 'PG',
1630
+ 'MA',
1631
+ 'UNH',
1632
+ 'HD',
1633
+ 'DIS',
1634
+ 'BAC',
1635
+ 'XOM',
1636
+ 'PFE',
1637
+ 'KO',
1638
+ 'PEP',
1639
+ 'CSCO',
1640
+ 'ADBE',
1641
+ 'NFLX',
1642
+ 'INTC',
1643
+ 'CRM',
1644
+ 'ABT',
1645
+ 'NKE',
1646
+ 'MRK',
1647
+ 'TMO',
1648
+ 'ORCL',
1649
+ 'ACN',
1650
+ 'MDT',
1651
+ 'COST',
1652
+ 'LLY',
1653
+ 'AVGO',
1654
+ 'TXN',
1655
+ 'NEE',
1656
+ 'UNP',
1657
+ 'DHR',
1658
+ 'QCOM',
1659
+ 'PM',
1660
+ 'BMY',
1661
+ 'RTX',
1662
+ 'HON',
1663
+ 'UPS',
1664
+ 'LOW',
1665
+ 'AMGN',
1666
+ 'SBUX',
1667
+ 'IBM',
1668
+ ];
1669
+ const companies = [
1670
+ 'Apple Inc.',
1671
+ 'Microsoft Corp.',
1672
+ 'Alphabet Inc.',
1673
+ 'Amazon.com Inc.',
1674
+ 'Meta Platforms',
1675
+ 'Tesla Inc.',
1676
+ 'NVIDIA Corp.',
1677
+ 'JPMorgan Chase',
1678
+ 'Visa Inc.',
1679
+ 'Johnson & Johnson',
1680
+ 'Walmart Inc.',
1681
+ 'Procter & Gamble',
1682
+ 'Mastercard',
1683
+ 'UnitedHealth',
1684
+ 'Home Depot',
1685
+ 'Walt Disney',
1686
+ 'Bank of America',
1687
+ 'Exxon Mobil',
1688
+ 'Pfizer Inc.',
1689
+ 'Coca-Cola',
1690
+ 'PepsiCo Inc.',
1691
+ 'Cisco Systems',
1692
+ 'Adobe Inc.',
1693
+ 'Netflix Inc.',
1694
+ 'Intel Corp.',
1695
+ 'Salesforce',
1696
+ 'Abbott Labs',
1697
+ 'Nike Inc.',
1698
+ 'Merck & Co.',
1699
+ 'Thermo Fisher',
1700
+ 'Oracle Corp.',
1701
+ 'Accenture',
1702
+ 'Medtronic',
1703
+ 'Costco',
1704
+ 'Eli Lilly',
1705
+ 'Broadcom',
1706
+ 'Texas Instruments and technology and HSBC',
1707
+ 'NextEra Energy',
1708
+ 'Union Pacific',
1709
+ 'Danaher',
1710
+ 'Qualcomm',
1711
+ 'Philip Morris',
1712
+ 'Bristol-Myers',
1713
+ 'Raytheon',
1714
+ 'Honeywell',
1715
+ 'UPS',
1716
+ 'Lowes',
1717
+ 'Amgen',
1718
+ 'Starbucks',
1719
+ 'IBM',
1720
+ ];
1619
1721
  const sectors = ['Technology', 'Healthcare', 'Financials', 'Consumer', 'Energy', 'Industrials'];
1620
- const industries = ['Software', 'Hardware', 'Biotech', 'Banking', 'Retail', 'Semiconductors', 'Pharma', 'Media', 'Oil & Gas', 'Aerospace'];
1722
+ const industries = [
1723
+ 'Software',
1724
+ 'Hardware',
1725
+ 'Biotech',
1726
+ 'Banking',
1727
+ 'Retail',
1728
+ 'Semiconductors',
1729
+ 'Pharma',
1730
+ 'Media',
1731
+ 'Oil & Gas',
1732
+ 'Aerospace',
1733
+ ];
1621
1734
 
1622
1735
  const sampleStocks: StockData[] = Array.from({ length: 50 }, (_, i) => ({
1623
1736
  id: i + 1,
@@ -1644,31 +1757,171 @@ const sampleStocks: StockData[] = Array.from({ length: 50 }, (_, i) => ({
1644
1757
  const stockColumns: SpreadsheetColumn<StockData>[] = [
1645
1758
  { id: 'ticker', label: 'Ticker', width: 80, sortable: true, filterable: true },
1646
1759
  { id: 'company', label: 'Company', width: 180, sortable: true, filterable: true },
1647
- { id: 'sector', label: 'Sector', width: 120, sortable: true, filterable: true, type: 'select', options: sectors },
1760
+ {
1761
+ id: 'sector',
1762
+ label: 'Sector',
1763
+ width: 120,
1764
+ sortable: true,
1765
+ filterable: true,
1766
+ type: 'select',
1767
+ options: sectors,
1768
+ },
1648
1769
  { id: 'industry', label: 'Industry', width: 140, sortable: true, filterable: true },
1649
- { id: 'marketCap', label: 'Market Cap', width: 130, sortable: true, type: 'number', align: 'right', render: (v) => `$${(v / 1e9).toFixed(1)}B` },
1650
- { id: 'price', label: 'Price', width: 90, sortable: true, type: 'number', align: 'right', render: (v) => `$${v.toFixed(2)}` },
1651
- { id: 'change', label: 'Change', width: 90, sortable: true, type: 'number', align: 'right', render: (v) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}` },
1652
- { id: 'changePercent', label: 'Change %', width: 100, sortable: true, type: 'number', align: 'right', render: (v) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}%` },
1653
- { id: 'volume', label: 'Volume', width: 110, sortable: true, type: 'number', align: 'right', render: (v) => `${(v / 1e6).toFixed(1)}M` },
1654
- { id: 'avgVolume', label: 'Avg Volume', width: 110, sortable: true, type: 'number', align: 'right', render: (v) => `${(v / 1e6).toFixed(1)}M` },
1770
+ {
1771
+ id: 'marketCap',
1772
+ label: 'Market Cap',
1773
+ width: 130,
1774
+ sortable: true,
1775
+ type: 'number',
1776
+ align: 'right',
1777
+ render: (v) => `$${(v / 1e9).toFixed(1)}B`,
1778
+ },
1779
+ {
1780
+ id: 'price',
1781
+ label: 'Price',
1782
+ width: 90,
1783
+ sortable: true,
1784
+ type: 'number',
1785
+ align: 'right',
1786
+ render: (v) => `$${v.toFixed(2)}`,
1787
+ },
1788
+ {
1789
+ id: 'change',
1790
+ label: 'Change',
1791
+ width: 90,
1792
+ sortable: true,
1793
+ type: 'number',
1794
+ align: 'right',
1795
+ render: (v) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}`,
1796
+ },
1797
+ {
1798
+ id: 'changePercent',
1799
+ label: 'Change %',
1800
+ width: 100,
1801
+ sortable: true,
1802
+ type: 'number',
1803
+ align: 'right',
1804
+ render: (v) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}%`,
1805
+ },
1806
+ {
1807
+ id: 'volume',
1808
+ label: 'Volume',
1809
+ width: 110,
1810
+ sortable: true,
1811
+ type: 'number',
1812
+ align: 'right',
1813
+ render: (v) => `${(v / 1e6).toFixed(1)}M`,
1814
+ },
1815
+ {
1816
+ id: 'avgVolume',
1817
+ label: 'Avg Volume',
1818
+ width: 110,
1819
+ sortable: true,
1820
+ type: 'number',
1821
+ align: 'right',
1822
+ render: (v) => `${(v / 1e6).toFixed(1)}M`,
1823
+ },
1655
1824
  { id: 'pe', label: 'P/E', width: 80, sortable: true, type: 'number', align: 'right' },
1656
- { id: 'eps', label: 'EPS', width: 80, sortable: true, type: 'number', align: 'right', render: (v) => `$${v.toFixed(2)}` },
1657
- { id: 'dividend', label: 'Dividend %', width: 100, sortable: true, type: 'number', align: 'right', render: (v) => `${v.toFixed(2)}%` },
1825
+ {
1826
+ id: 'eps',
1827
+ label: 'EPS',
1828
+ width: 80,
1829
+ sortable: true,
1830
+ type: 'number',
1831
+ align: 'right',
1832
+ render: (v) => `$${v.toFixed(2)}`,
1833
+ },
1834
+ {
1835
+ id: 'dividend',
1836
+ label: 'Dividend %',
1837
+ width: 100,
1838
+ sortable: true,
1839
+ type: 'number',
1840
+ align: 'right',
1841
+ render: (v) => `${v.toFixed(2)}%`,
1842
+ },
1658
1843
  { id: 'beta', label: 'Beta', width: 70, sortable: true, type: 'number', align: 'right' },
1659
- { id: 'week52High', label: '52W High', width: 100, sortable: true, type: 'number', align: 'right', render: (v) => `$${v.toFixed(2)}` },
1660
- { id: 'week52Low', label: '52W Low', width: 100, sortable: true, type: 'number', align: 'right', render: (v) => `$${v.toFixed(2)}` },
1661
- { id: 'revenue', label: 'Revenue', width: 120, sortable: true, type: 'number', align: 'right', render: (v) => `$${(v / 1e9).toFixed(1)}B` },
1662
- { id: 'profit', label: 'Net Income', width: 120, sortable: true, type: 'number', align: 'right', render: (v) => `$${(v / 1e9).toFixed(1)}B` },
1844
+ {
1845
+ id: 'week52High',
1846
+ label: '52W High',
1847
+ width: 100,
1848
+ sortable: true,
1849
+ type: 'number',
1850
+ align: 'right',
1851
+ render: (v) => `$${v.toFixed(2)}`,
1852
+ },
1853
+ {
1854
+ id: 'week52Low',
1855
+ label: '52W Low',
1856
+ width: 100,
1857
+ sortable: true,
1858
+ type: 'number',
1859
+ align: 'right',
1860
+ render: (v) => `$${v.toFixed(2)}`,
1861
+ },
1862
+ {
1863
+ id: 'revenue',
1864
+ label: 'Revenue',
1865
+ width: 120,
1866
+ sortable: true,
1867
+ type: 'number',
1868
+ align: 'right',
1869
+ render: (v) => `$${(v / 1e9).toFixed(1)}B`,
1870
+ },
1871
+ {
1872
+ id: 'profit',
1873
+ label: 'Net Income',
1874
+ width: 120,
1875
+ sortable: true,
1876
+ type: 'number',
1877
+ align: 'right',
1878
+ render: (v) => `$${(v / 1e9).toFixed(1)}B`,
1879
+ },
1663
1880
  ];
1664
1881
 
1665
1882
  const stockColumnGroups: SpreadsheetColumnGroup[] = [
1666
- { id: 'identity', label: 'Identity', columns: ['ticker', 'company'], collapsible: true, headerColor: '#dbeafe' },
1667
- { id: 'classification', label: 'Classification', columns: ['sector', 'industry'], collapsible: true, headerColor: '#fef3c7' },
1668
- { id: 'market', label: 'Market Data', columns: ['marketCap', 'price', 'change', 'changePercent', 'volume', 'avgVolume'], collapsible: true, headerColor: '#d1fae5' },
1669
- { id: 'fundamentals', label: 'Fundamentals', columns: ['pe', 'eps', 'dividend', 'beta'], collapsible: true, headerColor: '#fce7f3' },
1670
- { id: 'range', label: '52-Week Range', columns: ['week52High', 'week52Low'], collapsible: true, headerColor: '#e0e7ff' },
1671
- { id: 'financials', label: 'Financials', columns: ['revenue', 'profit'], collapsible: true, headerColor: '#fef9c3' },
1883
+ {
1884
+ id: 'identity',
1885
+ label: 'Identity',
1886
+ columns: ['ticker', 'company'],
1887
+ collapsible: true,
1888
+ headerColor: '#dbeafe',
1889
+ },
1890
+ {
1891
+ id: 'classification',
1892
+ label: 'Classification',
1893
+ columns: ['sector', 'industry'],
1894
+ collapsible: true,
1895
+ headerColor: '#fef3c7',
1896
+ },
1897
+ {
1898
+ id: 'market',
1899
+ label: 'Market Data',
1900
+ columns: ['marketCap', 'price', 'change', 'changePercent', 'volume', 'avgVolume'],
1901
+ collapsible: true,
1902
+ headerColor: '#d1fae5',
1903
+ },
1904
+ {
1905
+ id: 'fundamentals',
1906
+ label: 'Fundamentals',
1907
+ columns: ['pe', 'eps', 'dividend', 'beta'],
1908
+ collapsible: true,
1909
+ headerColor: '#fce7f3',
1910
+ },
1911
+ {
1912
+ id: 'range',
1913
+ label: '52-Week Range',
1914
+ columns: ['week52High', 'week52Low'],
1915
+ collapsible: true,
1916
+ headerColor: '#e0e7ff',
1917
+ },
1918
+ {
1919
+ id: 'financials',
1920
+ label: 'Financials',
1921
+ columns: ['revenue', 'profit'],
1922
+ collapsible: true,
1923
+ headerColor: '#fef9c3',
1924
+ },
1672
1925
  ];
1673
1926
 
1674
1927
  export const PinnedColumnsWithGroups: Story = {
@@ -1686,10 +1939,17 @@ export const PinnedColumnsWithGroups: Story = {
1686
1939
  columns.
1687
1940
  </p>
1688
1941
  <ul className="text-sm text-blue-700 space-y-1 ml-4 list-disc">
1689
- <li><strong>Ticker & Company</strong> are pinned to the left</li>
1690
- <li><strong>19 columns</strong> across 6 collapsible groups</li>
1942
+ <li>
1943
+ <strong>Ticker & Company</strong> are pinned to the left
1944
+ </li>
1945
+ <li>
1946
+ <strong>19 columns</strong> across 6 collapsible groups
1947
+ </li>
1691
1948
  <li>Click the pin icon on any column header to pin/unpin</li>
1692
- <li>Collapse groups to verify pinned columns from collapsed groups still work</li>
1949
+ <li>
1950
+ Collapse groups to verify pinned columns from collapsed groups still
1951
+ work
1952
+ </li>
1693
1953
  </ul>
1694
1954
  </div>
1695
1955
 
@@ -21,12 +21,18 @@ import {
21
21
  useSpreadsheetPinning,
22
22
  ROW_INDEX_COLUMN_WIDTH,
23
23
  ROW_INDEX_COLUMN_ID,
24
+ MIN_PINNED_COLUMN_WIDTH,
24
25
  } from '../hooks/useSpreadsheetPinning';
25
26
  import { useSpreadsheetComments } from '../hooks/useSpreadsheetComments';
26
27
  import { useSpreadsheetUndoRedo } from '../hooks/useSpreadsheetUndoRedo';
27
28
  import { useSpreadsheetKeyboardShortcuts } from '../hooks/useSpreadsheetKeyboardShortcuts';
28
29
  import { useSpreadsheetSelection } from '../hooks/useSpreadsheetSelection';
29
- import type { CellEdit, SpreadsheetColumn, SpreadsheetColumnGroup, SpreadsheetProps } from '../types';
30
+ import type {
31
+ CellEdit,
32
+ SpreadsheetColumn,
33
+ SpreadsheetColumnGroup,
34
+ SpreadsheetProps,
35
+ } from '../types';
30
36
 
31
37
  type SingleCellEdit = {
32
38
  rowId: string | number;
@@ -645,7 +651,11 @@ export function Spreadsheet<T extends Record<string, any>>({
645
651
  }
646
652
 
647
653
  type ColumnItem = { type: 'column'; column: SpreadsheetColumn<T> };
648
- type PlaceholderItem = { type: 'collapsed-placeholder'; groupId: string; headerColor?: string };
654
+ type PlaceholderItem = {
655
+ type: 'collapsed-placeholder';
656
+ groupId: string;
657
+ headerColor?: string;
658
+ };
649
659
  type RenderItem = ColumnItem | PlaceholderItem;
650
660
 
651
661
  const leftPinnedItems: ColumnItem[] = [];
@@ -683,9 +693,7 @@ export function Spreadsheet<T extends Record<string, any>>({
683
693
  }
684
694
 
685
695
  // Add any columns not in any group
686
- const allGroupedIds = new Set(
687
- columnGroups.flatMap((g) => g.columns)
688
- );
696
+ const allGroupedIds = new Set(columnGroups.flatMap((g) => g.columns));
689
697
  for (const col of visibleColumns) {
690
698
  if (!allGroupedIds.has(col.id)) {
691
699
  const pinSide = pinnedColumns.get(col.id);
@@ -842,7 +850,7 @@ export function Spreadsheet<T extends Record<string, any>>({
842
850
  zoom: zoom / 100,
843
851
  }}
844
852
  >
845
- <table className="w-full border-separate border-spacing-0 text-xs select-none">
853
+ <table className="border-separate border-spacing-0 text-xs select-none">
846
854
  <thead>
847
855
  {/* Column Group Headers */}
848
856
  {columnGroups && groupHeaderItems && (
@@ -859,10 +867,9 @@ export function Spreadsheet<T extends Record<string, any>>({
859
867
  />
860
868
  {groupHeaderItems.map((item) => {
861
869
  if (item.type === 'pinned-column') {
862
- const col = columns.find(
863
- (c) => c.id === item.columnId
864
- );
870
+ const col = columns.find((c) => c.id === item.columnId);
865
871
  const isPinnedLeft = item.pinSide === 'left';
872
+ const pinnedWidth = Math.max(col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH);
866
873
  return (
867
874
  <th
868
875
  key={`pinned-group-${item.columnId}`}
@@ -872,8 +879,7 @@ export function Spreadsheet<T extends Record<string, any>>({
872
879
  )}
873
880
  style={{
874
881
  backgroundColor:
875
- item.headerColor ||
876
- 'rgb(243 244 246)',
882
+ item.headerColor || 'rgb(243 244 246)',
877
883
  position: 'sticky',
878
884
  left: isPinnedLeft
879
885
  ? `${getColumnLeftOffset(item.columnId)}px`
@@ -881,12 +887,9 @@ export function Spreadsheet<T extends Record<string, any>>({
881
887
  right: !isPinnedLeft
882
888
  ? `${getColumnRightOffset(item.columnId)}px`
883
889
  : undefined,
884
- minWidth:
885
- col?.minWidth || col?.width,
886
- width:
887
- col?.minWidth || col?.width,
888
- maxWidth:
889
- col?.minWidth || col?.width,
890
+ minWidth: pinnedWidth,
891
+ width: pinnedWidth,
892
+ maxWidth: pinnedWidth,
890
893
  }}
891
894
  />
892
895
  );
@@ -947,8 +950,7 @@ export function Spreadsheet<T extends Record<string, any>>({
947
950
  className="border border-gray-200 px-2 py-1 text-center text-gray-400"
948
951
  style={{
949
952
  backgroundColor:
950
- item.headerColor ||
951
- 'rgb(243 244 246)',
953
+ item.headerColor || 'rgb(243 244 246)',
952
954
  minWidth: '30px',
953
955
  }}
954
956
  >
@@ -4,6 +4,7 @@ import { AiFillHighlight } from 'react-icons/ai';
4
4
  import { FaComment, FaRegComment } from 'react-icons/fa';
5
5
  import { cn } from '../utils';
6
6
  import type { SpreadsheetCellProps } from '../types';
7
+ import { MIN_PINNED_COLUMN_WIDTH } from '../hooks/useSpreadsheetPinning';
7
8
 
8
9
  const cellPaddingCompact = 'px-1 py-px';
9
10
  const cellPaddingNormal = 'px-2 py-1';
@@ -273,10 +274,11 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
273
274
  style={{
274
275
  backgroundColor: isInSelection ? 'rgb(239 246 255)' : getBackgroundColor(),
275
276
  minWidth: column.minWidth || column.width,
276
- // Pinned columns need fixed width so sticky offset calculations are accurate
277
+ // Pinned columns must have a fixed width so sticky offsets stay correct.
278
+ // Enforce MIN_PINNED_COLUMN_WIDTH so header actions always fit.
277
279
  ...(isPinned && {
278
- width: column.minWidth || column.width,
279
- maxWidth: column.minWidth || column.width,
280
+ width: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
281
+ maxWidth: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
280
282
  }),
281
283
  ...positionStyles,
282
284
  ...selectionBorderStyles,
@@ -3,6 +3,7 @@ import { HiChevronDown, HiChevronUp } from 'react-icons/hi';
3
3
  import { cn } from '../utils';
4
4
  import type { SpreadsheetHeaderProps } from '../types';
5
5
  import { ColumnHeaderActions } from './ColumnHeaderActions';
6
+ import { MIN_PINNED_COLUMN_WIDTH } from '../hooks/useSpreadsheetPinning';
6
7
 
7
8
  const cellPaddingCompact = 'px-1 py-0.5';
8
9
  const cellPaddingNormal = 'px-2 py-1.5';
@@ -71,10 +72,11 @@ export const SpreadsheetHeader: React.FC<
71
72
  style={{
72
73
  backgroundColor: highlightColor || 'rgb(243 244 246)', // gray-100
73
74
  minWidth: column.minWidth || column.width,
74
- // Pinned columns need fixed width so sticky offset calculations are accurate
75
+ // Pinned columns must have a fixed width so sticky offsets stay correct.
76
+ // Enforce MIN_PINNED_COLUMN_WIDTH so header actions (pin/filter/highlight) always fit.
75
77
  ...(isPinned && {
76
- width: column.minWidth || column.width,
77
- maxWidth: column.minWidth || column.width,
78
+ width: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
79
+ maxWidth: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
78
80
  }),
79
81
  top: 0, // For sticky header
80
82
  ...positionStyles,
@@ -4,6 +4,8 @@ import type { SpreadsheetColumn, SpreadsheetColumnGroup } from '../types';
4
4
  // Special column ID for row index
5
5
  export const ROW_INDEX_COLUMN_ID = '__row_index__';
6
6
  export const ROW_INDEX_COLUMN_WIDTH = 80;
7
+ // Minimum width for any pinned column to ensure header actions (pin, filter, highlight icons) fit
8
+ export const MIN_PINNED_COLUMN_WIDTH = 100;
7
9
 
8
10
  export interface UseSpreadsheetPinningOptions<T> {
9
11
  columns: SpreadsheetColumn<T>[];
@@ -178,8 +180,10 @@ export function useSpreadsheetPinning<T>({
178
180
  let offset = baseOffset;
179
181
  for (let i = 0; i < index; i++) {
180
182
  const col = columns.find((c) => c.id === pinnedLeft[i]);
181
- // Use minWidth || width to match the rendered cell width
182
- offset += col?.minWidth || col?.width || 100;
183
+ // Pinned columns are clamped to at least MIN_PINNED_COLUMN_WIDTH
184
+ // so that header actions (pin, filter, highlight icons) always fit
185
+ const configuredWidth = col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH;
186
+ offset += Math.max(configuredWidth, MIN_PINNED_COLUMN_WIDTH);
183
187
  }
184
188
  return offset;
185
189
  },
@@ -218,7 +222,8 @@ export function useSpreadsheetPinning<T>({
218
222
  let offset = 0;
219
223
  for (let i = pinnedRight.length - 1; i > index; i--) {
220
224
  const col = columns.find((c) => c.id === pinnedRight[i]);
221
- offset += col?.minWidth || col?.width || 100;
225
+ const configuredWidth = col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH;
226
+ offset += Math.max(configuredWidth, MIN_PINNED_COLUMN_WIDTH);
222
227
  }
223
228
  return offset;
224
229
  },