@xcelsior/ui-spreadsheets 1.1.10 → 1.1.12
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/dist/index.js +119 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +119 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Spreadsheet.stories.tsx +121 -0
- package/src/components/Spreadsheet.tsx +161 -27
- package/src/components/SpreadsheetCell.tsx +5 -0
- package/src/components/SpreadsheetHeader.tsx +5 -0
package/package.json
CHANGED
|
@@ -1591,6 +1591,127 @@ export const WithCheckboxColumns: Story = {
|
|
|
1591
1591
|
},
|
|
1592
1592
|
};
|
|
1593
1593
|
|
|
1594
|
+
// Pinned columns with many columns and column groups
|
|
1595
|
+
interface StockData {
|
|
1596
|
+
id: number;
|
|
1597
|
+
ticker: string;
|
|
1598
|
+
company: string;
|
|
1599
|
+
sector: string;
|
|
1600
|
+
industry: string;
|
|
1601
|
+
marketCap: number;
|
|
1602
|
+
price: number;
|
|
1603
|
+
change: number;
|
|
1604
|
+
changePercent: number;
|
|
1605
|
+
volume: number;
|
|
1606
|
+
avgVolume: number;
|
|
1607
|
+
pe: number;
|
|
1608
|
+
eps: number;
|
|
1609
|
+
dividend: number;
|
|
1610
|
+
beta: number;
|
|
1611
|
+
week52High: number;
|
|
1612
|
+
week52Low: number;
|
|
1613
|
+
revenue: number;
|
|
1614
|
+
profit: number;
|
|
1615
|
+
}
|
|
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'];
|
|
1619
|
+
const sectors = ['Technology', 'Healthcare', 'Financials', 'Consumer', 'Energy', 'Industrials'];
|
|
1620
|
+
const industries = ['Software', 'Hardware', 'Biotech', 'Banking', 'Retail', 'Semiconductors', 'Pharma', 'Media', 'Oil & Gas', 'Aerospace'];
|
|
1621
|
+
|
|
1622
|
+
const sampleStocks: StockData[] = Array.from({ length: 50 }, (_, i) => ({
|
|
1623
|
+
id: i + 1,
|
|
1624
|
+
ticker: tickers[i],
|
|
1625
|
+
company: companies[i],
|
|
1626
|
+
sector: sectors[i % sectors.length],
|
|
1627
|
+
industry: industries[i % industries.length],
|
|
1628
|
+
marketCap: Math.round((50 + Math.random() * 2950) * 1e9),
|
|
1629
|
+
price: Math.round((20 + Math.random() * 480) * 100) / 100,
|
|
1630
|
+
change: Math.round((Math.random() * 20 - 10) * 100) / 100,
|
|
1631
|
+
changePercent: Math.round((Math.random() * 10 - 5) * 100) / 100,
|
|
1632
|
+
volume: Math.round(Math.random() * 50e6),
|
|
1633
|
+
avgVolume: Math.round(Math.random() * 40e6),
|
|
1634
|
+
pe: Math.round((5 + Math.random() * 60) * 100) / 100,
|
|
1635
|
+
eps: Math.round((0.5 + Math.random() * 15) * 100) / 100,
|
|
1636
|
+
dividend: Math.round(Math.random() * 5 * 100) / 100,
|
|
1637
|
+
beta: Math.round((0.5 + Math.random() * 1.5) * 100) / 100,
|
|
1638
|
+
week52High: Math.round((100 + Math.random() * 400) * 100) / 100,
|
|
1639
|
+
week52Low: Math.round((20 + Math.random() * 200) * 100) / 100,
|
|
1640
|
+
revenue: Math.round(Math.random() * 400e9),
|
|
1641
|
+
profit: Math.round(Math.random() * 80e9),
|
|
1642
|
+
}));
|
|
1643
|
+
|
|
1644
|
+
const stockColumns: SpreadsheetColumn<StockData>[] = [
|
|
1645
|
+
{ id: 'ticker', label: 'Ticker', width: 80, sortable: true, filterable: true },
|
|
1646
|
+
{ 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 },
|
|
1648
|
+
{ 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` },
|
|
1655
|
+
{ 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)}%` },
|
|
1658
|
+
{ 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` },
|
|
1663
|
+
];
|
|
1664
|
+
|
|
1665
|
+
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' },
|
|
1672
|
+
];
|
|
1673
|
+
|
|
1674
|
+
export const PinnedColumnsWithGroups: Story = {
|
|
1675
|
+
render: () => {
|
|
1676
|
+
return (
|
|
1677
|
+
<div className="p-4">
|
|
1678
|
+
<div className="mb-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
|
1679
|
+
<h3 className="font-semibold text-blue-900 mb-2">
|
|
1680
|
+
Pinned Columns with Column Groups
|
|
1681
|
+
</h3>
|
|
1682
|
+
<p className="text-sm text-blue-700 mb-3">
|
|
1683
|
+
This example tests pinned columns with many columns and column groups.
|
|
1684
|
+
Ticker and Company are pinned to the left. Scroll horizontally to verify
|
|
1685
|
+
pinned columns stay in place. Try collapsing groups and pinning/unpinning
|
|
1686
|
+
columns.
|
|
1687
|
+
</p>
|
|
1688
|
+
<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>
|
|
1691
|
+
<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>
|
|
1693
|
+
</ul>
|
|
1694
|
+
</div>
|
|
1695
|
+
|
|
1696
|
+
<Spreadsheet
|
|
1697
|
+
data={sampleStocks}
|
|
1698
|
+
columns={stockColumns}
|
|
1699
|
+
columnGroups={stockColumnGroups}
|
|
1700
|
+
getRowId={(row) => row.id}
|
|
1701
|
+
settings={{
|
|
1702
|
+
defaultPinnedColumns: ['__row_index__', 'ticker', 'company'],
|
|
1703
|
+
}}
|
|
1704
|
+
showToolbar
|
|
1705
|
+
showPagination
|
|
1706
|
+
enableRowSelection
|
|
1707
|
+
enableCellEditing={false}
|
|
1708
|
+
pageSizeOptions={[25, 50]}
|
|
1709
|
+
/>
|
|
1710
|
+
</div>
|
|
1711
|
+
);
|
|
1712
|
+
},
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1594
1715
|
// With Right-Pinned Columns
|
|
1595
1716
|
export const WithRightPinnedColumns: Story = {
|
|
1596
1717
|
render: () => {
|
|
@@ -26,7 +26,7 @@ import { useSpreadsheetComments } from '../hooks/useSpreadsheetComments';
|
|
|
26
26
|
import { useSpreadsheetUndoRedo } from '../hooks/useSpreadsheetUndoRedo';
|
|
27
27
|
import { useSpreadsheetKeyboardShortcuts } from '../hooks/useSpreadsheetKeyboardShortcuts';
|
|
28
28
|
import { useSpreadsheetSelection } from '../hooks/useSpreadsheetSelection';
|
|
29
|
-
import type { CellEdit, SpreadsheetColumn, SpreadsheetProps } from '../types';
|
|
29
|
+
import type { CellEdit, SpreadsheetColumn, SpreadsheetColumnGroup, SpreadsheetProps } from '../types';
|
|
30
30
|
|
|
31
31
|
type SingleCellEdit = {
|
|
32
32
|
rowId: string | number;
|
|
@@ -635,6 +635,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
635
635
|
}, [setHighlightPickerColumn]);
|
|
636
636
|
|
|
637
637
|
// Build render items that include placeholder cells for collapsed groups
|
|
638
|
+
// Pinned columns are moved to the edges: left-pinned first, then unpinned, then right-pinned
|
|
638
639
|
const columnRenderItems = useMemo(() => {
|
|
639
640
|
if (!columnGroups || columnGroups.length === 0) {
|
|
640
641
|
return visibleColumns.map((col) => ({
|
|
@@ -643,20 +644,19 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
643
644
|
}));
|
|
644
645
|
}
|
|
645
646
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
> = [];
|
|
647
|
+
type ColumnItem = { type: 'column'; column: SpreadsheetColumn<T> };
|
|
648
|
+
type PlaceholderItem = { type: 'collapsed-placeholder'; groupId: string; headerColor?: string };
|
|
649
|
+
type RenderItem = ColumnItem | PlaceholderItem;
|
|
650
|
+
|
|
651
|
+
const leftPinnedItems: ColumnItem[] = [];
|
|
652
|
+
const middleItems: RenderItem[] = [];
|
|
653
|
+
const rightPinnedItems: ColumnItem[] = [];
|
|
654
654
|
|
|
655
655
|
for (const group of columnGroups) {
|
|
656
656
|
const isCollapsed = collapsedGroups.has(group.id);
|
|
657
657
|
|
|
658
658
|
if (isCollapsed) {
|
|
659
|
-
|
|
659
|
+
middleItems.push({
|
|
660
660
|
type: 'collapsed-placeholder',
|
|
661
661
|
groupId: group.id,
|
|
662
662
|
headerColor: group.headerColor,
|
|
@@ -671,7 +671,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
671
671
|
});
|
|
672
672
|
|
|
673
673
|
for (const col of groupVisibleCols) {
|
|
674
|
-
|
|
674
|
+
const pinSide = pinnedColumns.get(col.id);
|
|
675
|
+
if (pinSide === 'left') {
|
|
676
|
+
leftPinnedItems.push({ type: 'column', column: col });
|
|
677
|
+
} else if (pinSide === 'right') {
|
|
678
|
+
rightPinnedItems.push({ type: 'column', column: col });
|
|
679
|
+
} else {
|
|
680
|
+
middleItems.push({ type: 'column', column: col });
|
|
681
|
+
}
|
|
675
682
|
}
|
|
676
683
|
}
|
|
677
684
|
|
|
@@ -681,13 +688,118 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
681
688
|
);
|
|
682
689
|
for (const col of visibleColumns) {
|
|
683
690
|
if (!allGroupedIds.has(col.id)) {
|
|
684
|
-
|
|
691
|
+
const pinSide = pinnedColumns.get(col.id);
|
|
692
|
+
if (pinSide === 'left') {
|
|
693
|
+
leftPinnedItems.push({ type: 'column', column: col });
|
|
694
|
+
} else if (pinSide === 'right') {
|
|
695
|
+
rightPinnedItems.push({ type: 'column', column: col });
|
|
696
|
+
} else {
|
|
697
|
+
middleItems.push({ type: 'column', column: col });
|
|
698
|
+
}
|
|
685
699
|
}
|
|
686
700
|
}
|
|
687
701
|
|
|
688
|
-
|
|
702
|
+
// Sort pinned items by their order in the pinnedColumns map
|
|
703
|
+
const pinnedLeftOrder = Array.from(pinnedColumns.entries())
|
|
704
|
+
.filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
|
|
705
|
+
.map(([id]) => id);
|
|
706
|
+
const pinnedRightOrder = Array.from(pinnedColumns.entries())
|
|
707
|
+
.filter(([, side]) => side === 'right')
|
|
708
|
+
.map(([id]) => id);
|
|
709
|
+
|
|
710
|
+
leftPinnedItems.sort(
|
|
711
|
+
(a, b) => pinnedLeftOrder.indexOf(a.column.id) - pinnedLeftOrder.indexOf(b.column.id)
|
|
712
|
+
);
|
|
713
|
+
rightPinnedItems.sort(
|
|
714
|
+
(a, b) => pinnedRightOrder.indexOf(a.column.id) - pinnedRightOrder.indexOf(b.column.id)
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
return [...leftPinnedItems, ...middleItems, ...rightPinnedItems];
|
|
689
718
|
}, [columnGroups, collapsedGroups, columns, pinnedColumns, visibleColumns]);
|
|
690
719
|
|
|
720
|
+
// Build group header items that account for pinned columns being moved to edges
|
|
721
|
+
const groupHeaderItems = useMemo(() => {
|
|
722
|
+
if (!columnGroups || columnGroups.length === 0) return null;
|
|
723
|
+
|
|
724
|
+
type PinnedGroupHeaderItem = {
|
|
725
|
+
type: 'pinned-column';
|
|
726
|
+
columnId: string;
|
|
727
|
+
headerColor?: string;
|
|
728
|
+
pinSide: 'left' | 'right';
|
|
729
|
+
};
|
|
730
|
+
type GroupHeaderItem = {
|
|
731
|
+
type: 'group';
|
|
732
|
+
group: SpreadsheetColumnGroup;
|
|
733
|
+
colSpan: number;
|
|
734
|
+
isCollapsed: boolean;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
const leftPinned: PinnedGroupHeaderItem[] = [];
|
|
738
|
+
const groups: GroupHeaderItem[] = [];
|
|
739
|
+
const rightPinned: PinnedGroupHeaderItem[] = [];
|
|
740
|
+
|
|
741
|
+
for (const group of columnGroups) {
|
|
742
|
+
const isCollapsed = collapsedGroups.has(group.id);
|
|
743
|
+
const groupColumns = (columns || []).filter((c) => group.columns.includes(c.id));
|
|
744
|
+
const visibleGroupColumns = isCollapsed
|
|
745
|
+
? groupColumns.filter((c) => pinnedColumns.has(c.id))
|
|
746
|
+
: groupColumns;
|
|
747
|
+
|
|
748
|
+
let movedLeftCount = 0;
|
|
749
|
+
let movedRightCount = 0;
|
|
750
|
+
|
|
751
|
+
for (const col of visibleGroupColumns) {
|
|
752
|
+
const pinSide = pinnedColumns.get(col.id);
|
|
753
|
+
if (pinSide === 'left') {
|
|
754
|
+
movedLeftCount++;
|
|
755
|
+
leftPinned.push({
|
|
756
|
+
type: 'pinned-column',
|
|
757
|
+
columnId: col.id,
|
|
758
|
+
headerColor: group.headerColor,
|
|
759
|
+
pinSide: 'left',
|
|
760
|
+
});
|
|
761
|
+
} else if (pinSide === 'right') {
|
|
762
|
+
movedRightCount++;
|
|
763
|
+
rightPinned.push({
|
|
764
|
+
type: 'pinned-column',
|
|
765
|
+
columnId: col.id,
|
|
766
|
+
headerColor: group.headerColor,
|
|
767
|
+
pinSide: 'right',
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const remainingCols = visibleGroupColumns.length - movedLeftCount - movedRightCount;
|
|
773
|
+
const colSpan = remainingCols + (isCollapsed ? 1 : 0);
|
|
774
|
+
|
|
775
|
+
if (colSpan > 0) {
|
|
776
|
+
groups.push({
|
|
777
|
+
type: 'group',
|
|
778
|
+
group,
|
|
779
|
+
colSpan,
|
|
780
|
+
isCollapsed,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Sort pinned items by their order in the pinnedColumns map
|
|
786
|
+
const pinnedLeftOrder = Array.from(pinnedColumns.entries())
|
|
787
|
+
.filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
|
|
788
|
+
.map(([id]) => id);
|
|
789
|
+
const pinnedRightOrder = Array.from(pinnedColumns.entries())
|
|
790
|
+
.filter(([, side]) => side === 'right')
|
|
791
|
+
.map(([id]) => id);
|
|
792
|
+
|
|
793
|
+
leftPinned.sort(
|
|
794
|
+
(a, b) => pinnedLeftOrder.indexOf(a.columnId) - pinnedLeftOrder.indexOf(b.columnId)
|
|
795
|
+
);
|
|
796
|
+
rightPinned.sort(
|
|
797
|
+
(a, b) => pinnedRightOrder.indexOf(a.columnId) - pinnedRightOrder.indexOf(b.columnId)
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
return [...leftPinned, ...groups, ...rightPinned];
|
|
801
|
+
}, [columnGroups, collapsedGroups, columns, pinnedColumns]);
|
|
802
|
+
|
|
691
803
|
// ==================== RENDER ====================
|
|
692
804
|
|
|
693
805
|
return (
|
|
@@ -733,7 +845,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
733
845
|
<table className="w-full border-separate border-spacing-0 text-xs select-none">
|
|
734
846
|
<thead>
|
|
735
847
|
{/* Column Group Headers */}
|
|
736
|
-
{columnGroups && (
|
|
848
|
+
{columnGroups && groupHeaderItems && (
|
|
737
849
|
<tr>
|
|
738
850
|
{/* Row index column header (rowSpan=2 for groups) */}
|
|
739
851
|
<RowIndexColumnHeader
|
|
@@ -745,19 +857,41 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
745
857
|
hasColumnGroups={true}
|
|
746
858
|
compactMode={effectiveCompactMode}
|
|
747
859
|
/>
|
|
748
|
-
{
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
860
|
+
{groupHeaderItems.map((item) => {
|
|
861
|
+
if (item.type === 'pinned-column') {
|
|
862
|
+
const col = columns.find(
|
|
863
|
+
(c) => c.id === item.columnId
|
|
864
|
+
);
|
|
865
|
+
const isPinnedLeft = item.pinSide === 'left';
|
|
866
|
+
return (
|
|
867
|
+
<th
|
|
868
|
+
key={`pinned-group-${item.columnId}`}
|
|
869
|
+
className={cn(
|
|
870
|
+
'border border-gray-200 px-2 py-1.5 text-center font-bold text-gray-700',
|
|
871
|
+
'z-30'
|
|
872
|
+
)}
|
|
873
|
+
style={{
|
|
874
|
+
backgroundColor:
|
|
875
|
+
item.headerColor ||
|
|
876
|
+
'rgb(243 244 246)',
|
|
877
|
+
position: 'sticky',
|
|
878
|
+
left: isPinnedLeft
|
|
879
|
+
? `${getColumnLeftOffset(item.columnId)}px`
|
|
880
|
+
: undefined,
|
|
881
|
+
right: !isPinnedLeft
|
|
882
|
+
? `${getColumnRightOffset(item.columnId)}px`
|
|
883
|
+
: undefined,
|
|
884
|
+
minWidth:
|
|
885
|
+
col?.minWidth || col?.width,
|
|
886
|
+
width:
|
|
887
|
+
col?.minWidth || col?.width,
|
|
888
|
+
maxWidth:
|
|
889
|
+
col?.minWidth || col?.width,
|
|
890
|
+
}}
|
|
891
|
+
/>
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
const { group, colSpan, isCollapsed } = item;
|
|
761
895
|
return (
|
|
762
896
|
<th
|
|
763
897
|
key={group.id}
|
|
@@ -273,6 +273,11 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
273
273
|
style={{
|
|
274
274
|
backgroundColor: isInSelection ? 'rgb(239 246 255)' : getBackgroundColor(),
|
|
275
275
|
minWidth: column.minWidth || column.width,
|
|
276
|
+
// Pinned columns need fixed width so sticky offset calculations are accurate
|
|
277
|
+
...(isPinned && {
|
|
278
|
+
width: column.minWidth || column.width,
|
|
279
|
+
maxWidth: column.minWidth || column.width,
|
|
280
|
+
}),
|
|
276
281
|
...positionStyles,
|
|
277
282
|
...selectionBorderStyles,
|
|
278
283
|
}}
|
|
@@ -71,6 +71,11 @@ export const SpreadsheetHeader: React.FC<
|
|
|
71
71
|
style={{
|
|
72
72
|
backgroundColor: highlightColor || 'rgb(243 244 246)', // gray-100
|
|
73
73
|
minWidth: column.minWidth || column.width,
|
|
74
|
+
// Pinned columns need fixed width so sticky offset calculations are accurate
|
|
75
|
+
...(isPinned && {
|
|
76
|
+
width: column.minWidth || column.width,
|
|
77
|
+
maxWidth: column.minWidth || column.width,
|
|
78
|
+
}),
|
|
74
79
|
top: 0, // For sticky header
|
|
75
80
|
...positionStyles,
|
|
76
81
|
}}
|