@xcelsior/ui-spreadsheets 1.2.2 → 1.3.0
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/.omc/state/agent-replay-0cead415-b3bd-40fd-b199-47371946c4db.jsonl +25 -0
- package/.omc/state/idle-notif-cooldown.json +3 -0
- package/.omc/state/last-tool-error.json +7 -0
- package/.omc/state/mission-state.json +179 -0
- package/.omc/state/subagent-tracking.json +116 -0
- package/.turbo/turbo-build.log +28 -28
- package/.turbo/turbo-lint.log +140 -0
- package/dist/index.d.mts +94 -4
- package/dist/index.d.ts +94 -4
- package/dist/index.js +2133 -1156
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2023 -1048
- package/dist/index.mjs.map +1 -1
- package/dist/styles/globals.css +156 -16
- package/dist/styles/globals.css.map +1 -1
- package/package.json +1 -1
- package/plans/20260330-1230-spreadsheet-features/phase-01-types-and-duplicates-hook.md +73 -0
- package/plans/20260330-1230-spreadsheet-features/phase-02-filter-dropdown-portal.md +90 -0
- package/plans/20260330-1230-spreadsheet-features/phase-03-header-overflow-menu.md +101 -0
- package/plans/20260330-1230-spreadsheet-features/phase-04-integration.md +193 -0
- package/plans/20260330-1230-spreadsheet-features/plan.md +59 -0
- package/src/components/ColorPickerPopover.tsx +77 -32
- package/src/components/ColumnHeaderActions.tsx +241 -1
- package/src/components/RowIndexColumnHeader.tsx +13 -17
- package/src/components/SelectionSummaryBar.tsx +103 -0
- package/src/components/Spreadsheet.stories.tsx +254 -0
- package/src/components/Spreadsheet.tsx +234 -189
- package/src/components/SpreadsheetCell.tsx +280 -42
- package/src/components/SpreadsheetFilterDropdown.tsx +178 -13
- package/src/components/SpreadsheetHeader.tsx +79 -24
- package/src/components/SpreadsheetSettingsModal.tsx +4 -0
- package/src/hooks/useSpreadsheetColumnResize.ts +143 -0
- package/src/hooks/useSpreadsheetDuplicates.ts +149 -0
- package/src/hooks/useSpreadsheetFiltering.ts +18 -1
- package/src/hooks/useSpreadsheetHighlighting.ts +23 -3
- package/src/hooks/useSpreadsheetKeyboardShortcuts.ts +16 -0
- package/src/hooks/useSpreadsheetPinning.ts +148 -134
- package/src/hooks/useSpreadsheetSelection.ts +10 -22
- package/src/hooks/useSpreadsheetSummary.ts +68 -0
- package/src/index.ts +4 -1
- package/src/styles/globals.css +51 -0
- package/src/types.ts +50 -2
- package/storybook-static/assets/Color-YHDXOIA2-CtQurLnT.js +1 -0
- package/storybook-static/assets/DocsRenderer-CFRXHY34-oxrW8Hvo.js +575 -0
- package/storybook-static/assets/Spreadsheet.stories-DvhhzuK4.js +1357 -0
- package/storybook-static/assets/chunk-XP5HYGXS-BpfKkqn7.js +1 -0
- package/storybook-static/assets/entry-preview-CkBGHCAN.js +2 -0
- package/storybook-static/assets/entry-preview-docs-ugJb6pa8.js +46 -0
- package/storybook-static/assets/iframe-CPp2u3vg.js +211 -0
- package/storybook-static/assets/index-BB9bPxRC.js +24 -0
- package/storybook-static/assets/index-BQFlzFLk.js +9 -0
- package/storybook-static/assets/index-CtvPRVHf.js +9 -0
- package/storybook-static/assets/index-DgH-xKnr.js +11 -0
- package/storybook-static/assets/index-DrFu-skq.js +6 -0
- package/storybook-static/assets/index-DrdPSA1J.js +240 -0
- package/storybook-static/assets/index-DzFBShOR.js +20 -0
- package/storybook-static/assets/index-v-1boR4t.js +1 -0
- package/storybook-static/assets/preview-B8lJiyuQ.js +34 -0
- package/storybook-static/assets/preview-BBWR9nbA.js +1 -0
- package/storybook-static/assets/preview-BWzBA1C2.js +396 -0
- package/storybook-static/assets/preview-Bm0S-uxO.css +1 -0
- package/storybook-static/assets/preview-CvbIS5ZJ.js +1 -0
- package/storybook-static/assets/preview-DD_OYowb.js +1 -0
- package/storybook-static/assets/preview-DGUiP6tS.js +7 -0
- package/storybook-static/assets/preview-DHQbi4pV.js +1 -0
- package/storybook-static/assets/preview-DwI0w3cI.js +1 -0
- package/storybook-static/assets/preview-DyR7iiFG.js +1 -0
- package/storybook-static/assets/preview-zxZ6Be2V.js +2 -0
- package/storybook-static/assets/react-18-Pj8skaX9.js +1 -0
- package/storybook-static/assets/test-utils-quxJ1Z79.js +9 -0
- package/storybook-static/favicon.svg +1 -0
- package/storybook-static/iframe.html +666 -0
- package/storybook-static/index.html +177 -0
- package/storybook-static/index.json +1 -0
- package/storybook-static/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/project.json +1 -0
- package/storybook-static/sb-addons/essentials-actions-3/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-backgrounds-5/manager-bundle.js +12 -0
- package/storybook-static/sb-addons/essentials-controls-2/manager-bundle.js +405 -0
- package/storybook-static/sb-addons/essentials-docs-4/manager-bundle.js +245 -0
- package/storybook-static/sb-addons/essentials-measure-8/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-outline-9/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-toolbars-7/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/essentials-viewport-6/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/interactions-10/manager-bundle.js +222 -0
- package/storybook-static/sb-addons/links-1/manager-bundle.js +3 -0
- package/storybook-static/sb-addons/storybook-core-core-server-presets-0/common-manager-bundle.js +3 -0
- package/storybook-static/sb-common-assets/favicon.svg +1 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-bold.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-italic.woff2 +0 -0
- package/storybook-static/sb-common-assets/nunito-sans-regular.woff2 +0 -0
- package/storybook-static/sb-manager/globals-module-info.js +1052 -0
- package/storybook-static/sb-manager/globals-runtime.js +42127 -0
- package/storybook-static/sb-manager/globals.js +48 -0
- package/storybook-static/sb-manager/runtime.js +12048 -0
|
@@ -2,8 +2,8 @@ import { cn } from '../utils';
|
|
|
2
2
|
import { ColumnHeaderActions } from './ColumnHeaderActions';
|
|
3
3
|
import { ROW_INDEX_COLUMN_WIDTH } from '../hooks/useSpreadsheetPinning';
|
|
4
4
|
|
|
5
|
-
const cellPaddingCompact = 'px-1 py-
|
|
6
|
-
const cellPaddingNormal = 'px-2 py-
|
|
5
|
+
const cellPaddingCompact = 'px-1.5 py-1';
|
|
6
|
+
const cellPaddingNormal = 'px-2 py-2';
|
|
7
7
|
|
|
8
8
|
export interface RowIndexColumnHeaderProps {
|
|
9
9
|
/** Whether highlighting is enabled */
|
|
@@ -16,7 +16,7 @@ export interface RowIndexColumnHeaderProps {
|
|
|
16
16
|
onHighlightClick?: () => void;
|
|
17
17
|
/** Callback when pin button is clicked */
|
|
18
18
|
onPinClick?: () => void;
|
|
19
|
-
/** Whether this is the empty placeholder cell in the
|
|
19
|
+
/** Whether this is the empty placeholder cell in the group header row */
|
|
20
20
|
isSecondRow?: boolean;
|
|
21
21
|
/** Whether compact mode is enabled */
|
|
22
22
|
compactMode?: boolean;
|
|
@@ -27,6 +27,7 @@ export interface RowIndexColumnHeaderProps {
|
|
|
27
27
|
/**
|
|
28
28
|
* Row index column header (#) with highlight and pin actions.
|
|
29
29
|
* Uses the same ColumnHeaderActions component as regular columns for consistency.
|
|
30
|
+
* Vertical sticking is handled by the parent <thead>, this only handles horizontal sticking when pinned.
|
|
30
31
|
*/
|
|
31
32
|
export function RowIndexColumnHeader({
|
|
32
33
|
enableHighlighting = false,
|
|
@@ -40,24 +41,21 @@ export function RowIndexColumnHeader({
|
|
|
40
41
|
}: RowIndexColumnHeaderProps) {
|
|
41
42
|
const cellPadding = compactMode ? cellPaddingCompact : cellPaddingNormal;
|
|
42
43
|
|
|
43
|
-
//
|
|
44
|
-
// This should NOT be sticky vertically (no top: 0) - only the actual column header row should be sticky
|
|
44
|
+
// Group header row placeholder - always sticky left
|
|
45
45
|
if (isSecondRow) {
|
|
46
46
|
return (
|
|
47
47
|
<th
|
|
48
48
|
className={cn(
|
|
49
|
-
'border border-gray-200 text-center font-bold text-gray-700',
|
|
50
|
-
compactMode ? 'text-
|
|
49
|
+
'border border-gray-200 text-center font-bold text-gray-700 sticky',
|
|
50
|
+
compactMode ? 'text-xs' : 'text-sm',
|
|
51
51
|
cellPadding,
|
|
52
|
-
isPinned ? 'z-30' : 'z-20',
|
|
53
52
|
className
|
|
54
53
|
)}
|
|
55
54
|
style={{
|
|
56
55
|
minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
57
56
|
width: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
left: isPinned ? 0 : undefined,
|
|
57
|
+
left: 0,
|
|
58
|
+
zIndex: 50,
|
|
61
59
|
backgroundColor: highlightColor || 'rgb(243 244 246)',
|
|
62
60
|
}}
|
|
63
61
|
/>
|
|
@@ -67,18 +65,16 @@ export function RowIndexColumnHeader({
|
|
|
67
65
|
return (
|
|
68
66
|
<th
|
|
69
67
|
className={cn(
|
|
70
|
-
'border border-gray-200 text-center font-bold text-gray-700 group',
|
|
71
|
-
compactMode ? 'text-
|
|
68
|
+
'border border-gray-200 text-center font-bold text-gray-700 group sticky',
|
|
69
|
+
compactMode ? 'text-xs' : 'text-sm',
|
|
72
70
|
cellPadding,
|
|
73
|
-
isPinned ? 'z-30' : 'z-20',
|
|
74
71
|
className
|
|
75
72
|
)}
|
|
76
73
|
style={{
|
|
77
74
|
minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
78
75
|
width: `${ROW_INDEX_COLUMN_WIDTH}px`,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
left: isPinned ? 0 : undefined,
|
|
76
|
+
left: 0,
|
|
77
|
+
zIndex: 50,
|
|
82
78
|
backgroundColor: highlightColor || 'rgb(243 244 246)',
|
|
83
79
|
}}
|
|
84
80
|
>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { SelectionSummary } from '../hooks/useSpreadsheetSummary';
|
|
2
|
+
import type { CellPosition, SpreadsheetColumn } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface SelectionSummaryBarProps<T> {
|
|
5
|
+
/** Computed summary for numeric selections */
|
|
6
|
+
summary: SelectionSummary | null;
|
|
7
|
+
/** Currently focused cell */
|
|
8
|
+
focusedCell: CellPosition | null;
|
|
9
|
+
/** Column definitions */
|
|
10
|
+
columns: SpreadsheetColumn<T>[];
|
|
11
|
+
/** Current page's data */
|
|
12
|
+
data: T[];
|
|
13
|
+
/** Function to get row ID */
|
|
14
|
+
getRowId: (row: T) => string | number;
|
|
15
|
+
/** Current page number */
|
|
16
|
+
currentPage: number;
|
|
17
|
+
/** Page size */
|
|
18
|
+
pageSize: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Combined status bar showing cell address (left) and selection summary (right).
|
|
23
|
+
* Similar to Excel's bottom status bar.
|
|
24
|
+
*/
|
|
25
|
+
export function SelectionSummaryBar<T extends Record<string, any>>({
|
|
26
|
+
summary,
|
|
27
|
+
focusedCell,
|
|
28
|
+
columns,
|
|
29
|
+
data,
|
|
30
|
+
getRowId,
|
|
31
|
+
currentPage,
|
|
32
|
+
pageSize,
|
|
33
|
+
}: SelectionSummaryBarProps<T>) {
|
|
34
|
+
// Compute address info
|
|
35
|
+
let addressDisplay: string | null = null;
|
|
36
|
+
let valueDisplay: string | null = null;
|
|
37
|
+
|
|
38
|
+
if (focusedCell) {
|
|
39
|
+
const column = columns.find((c) => c.id === focusedCell.columnId);
|
|
40
|
+
const rowIndex = data.findIndex((r) => getRowId(r) === focusedCell.rowId);
|
|
41
|
+
const row = rowIndex !== -1 ? data[rowIndex] : null;
|
|
42
|
+
|
|
43
|
+
if (column && row) {
|
|
44
|
+
const displayRowIndex = rowIndex + 1 + (currentPage - 1) * pageSize;
|
|
45
|
+
addressDisplay = `Row ${displayRowIndex} / ${column.label}`;
|
|
46
|
+
const value = column.getValue ? column.getValue(row) : row[focusedCell.columnId];
|
|
47
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
48
|
+
valueDisplay = String(value);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Don't render if nothing to show
|
|
54
|
+
if (!addressDisplay && !summary) return null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex items-center justify-between px-3 py-1.5 bg-gray-50 border-t border-gray-200 text-xs text-gray-600">
|
|
58
|
+
{/* Left: Cell address + value */}
|
|
59
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
60
|
+
{addressDisplay && (
|
|
61
|
+
<>
|
|
62
|
+
<span className="font-medium text-gray-500 bg-white px-2 py-0.5 rounded border border-gray-200 shrink-0">
|
|
63
|
+
{addressDisplay}
|
|
64
|
+
</span>
|
|
65
|
+
{valueDisplay && (
|
|
66
|
+
<span className="text-gray-700 truncate" title={valueDisplay}>
|
|
67
|
+
{valueDisplay}
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Right: Selection summary */}
|
|
75
|
+
{summary && (
|
|
76
|
+
<div className="flex items-center gap-4 shrink-0">
|
|
77
|
+
<span>
|
|
78
|
+
<span className="text-gray-400 mr-1">Count:</span>
|
|
79
|
+
<span className="font-medium text-gray-700">{summary.numericCount}</span>
|
|
80
|
+
</span>
|
|
81
|
+
<span>
|
|
82
|
+
<span className="text-gray-400 mr-1">Sum:</span>
|
|
83
|
+
<span className="font-medium text-gray-700">{summary.sum.toLocaleString()}</span>
|
|
84
|
+
</span>
|
|
85
|
+
<span>
|
|
86
|
+
<span className="text-gray-400 mr-1">Avg:</span>
|
|
87
|
+
<span className="font-medium text-gray-700">{summary.avg.toLocaleString()}</span>
|
|
88
|
+
</span>
|
|
89
|
+
<span>
|
|
90
|
+
<span className="text-gray-400 mr-1">Min:</span>
|
|
91
|
+
<span className="font-medium text-gray-700">{summary.min.toLocaleString()}</span>
|
|
92
|
+
</span>
|
|
93
|
+
<span>
|
|
94
|
+
<span className="text-gray-400 mr-1">Max:</span>
|
|
95
|
+
<span className="font-medium text-gray-700">{summary.max.toLocaleString()}</span>
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
SelectionSummaryBar.displayName = 'SelectionSummaryBar';
|
|
@@ -2115,6 +2115,260 @@ export const PinnedColumnsWithGroups: Story = {
|
|
|
2115
2115
|
};
|
|
2116
2116
|
|
|
2117
2117
|
// With Right-Pinned Columns
|
|
2118
|
+
/**
|
|
2119
|
+
* Comprehensive test story that simulates a real-world logistics spreadsheet.
|
|
2120
|
+
* 30+ columns across 5 groups, editable cells, 200 rows — enough to test:
|
|
2121
|
+
* - Horizontal scrolling with many columns
|
|
2122
|
+
* - Double-click to edit
|
|
2123
|
+
* - Drag-to-select
|
|
2124
|
+
* - Ctrl/Cmd+A select all
|
|
2125
|
+
* - Zebra striping
|
|
2126
|
+
* - Sticky row index
|
|
2127
|
+
* - Enter to submit filter
|
|
2128
|
+
* - Compact vs normal mode toggle
|
|
2129
|
+
*/
|
|
2130
|
+
interface DocketRecord {
|
|
2131
|
+
id: number;
|
|
2132
|
+
rego: string;
|
|
2133
|
+
driver: string;
|
|
2134
|
+
type: string;
|
|
2135
|
+
date: string;
|
|
2136
|
+
job: string;
|
|
2137
|
+
customer: string;
|
|
2138
|
+
custInv: string;
|
|
2139
|
+
from: string;
|
|
2140
|
+
tipSite: string;
|
|
2141
|
+
comment: string;
|
|
2142
|
+
docket: string;
|
|
2143
|
+
custDocket: string;
|
|
2144
|
+
uom: string;
|
|
2145
|
+
qty: number;
|
|
2146
|
+
rate: number;
|
|
2147
|
+
totalPre: number;
|
|
2148
|
+
totalPost: number;
|
|
2149
|
+
tipName: string;
|
|
2150
|
+
tipUom: string;
|
|
2151
|
+
tipQty: number;
|
|
2152
|
+
tipRate: number;
|
|
2153
|
+
tipPre: number;
|
|
2154
|
+
tipPost: number;
|
|
2155
|
+
tipInvoice: string;
|
|
2156
|
+
truckPre: number;
|
|
2157
|
+
truckPost: number;
|
|
2158
|
+
subRate: number;
|
|
2159
|
+
subPre: number;
|
|
2160
|
+
subPost: number;
|
|
2161
|
+
subInvoice: string;
|
|
2162
|
+
weekEnding: string;
|
|
2163
|
+
profit: number;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const docketData: DocketRecord[] = Array.from({ length: 200 }, (_, i) => {
|
|
2167
|
+
const qty = Math.round((1 + Math.random() * 50) * 100) / 100;
|
|
2168
|
+
const rate = Math.round((10 + Math.random() * 90) * 100) / 100;
|
|
2169
|
+
const totalPre = Math.round(qty * rate * 100) / 100;
|
|
2170
|
+
const tipQty = Math.round((1 + Math.random() * 30) * 100) / 100;
|
|
2171
|
+
const tipRate = Math.round((5 + Math.random() * 40) * 100) / 100;
|
|
2172
|
+
const tipPre = Math.round(tipQty * tipRate * 100) / 100;
|
|
2173
|
+
const subRate = Math.round((20 + Math.random() * 60) * 100) / 100;
|
|
2174
|
+
const subPre = Math.round(qty * subRate * 100) / 100;
|
|
2175
|
+
const profit = Math.round((totalPre - tipPre - subPre) * 100) / 100;
|
|
2176
|
+
|
|
2177
|
+
return {
|
|
2178
|
+
id: i + 1,
|
|
2179
|
+
rego: ['SPR4RO', 'XO78SR', 'KO04KW', 'CL24SS', 'XP60LQ'][i % 5],
|
|
2180
|
+
driver: ['Donald Strathaam', 'James Urewis', 'Shaun Davis', 'Matthew Maddick', 'Steven Ayers'][i % 5],
|
|
2181
|
+
type: ['LNG', 'SUBBY'][i % 2],
|
|
2182
|
+
date: `2026-03-${String((i % 28) + 1).padStart(2, '0')}`,
|
|
2183
|
+
job: ['BORAL ALLOCATION', 'HOLCIM LYNWOOD - PEJAR', 'EARTHWORK FIVEDOCK', 'GAMUDA ECOR'][i % 4],
|
|
2184
|
+
customer: ['Boral Transport Ltd', 'Holcim (Australia) Pty Ltd', 'Earthworks Group', 'Gamuda Engineering'][i % 4],
|
|
2185
|
+
custInv: `INV-${String(i + 1).padStart(5, '0')}`,
|
|
2186
|
+
from: ['Unnamed Road, Marulan NSW 2579', 'Burrows Road South, Saint Peters NSW 2044', '25 Burrows Road South'][i % 3],
|
|
2187
|
+
tipSite: ['Goulburn Road, Pejar NSW 2583', 'Ada Avenue, Brookvale NSW 2100', '52 Bay Road, Taren Point'][i % 3],
|
|
2188
|
+
comment: i % 7 === 0 ? 'Check weight docket' : '',
|
|
2189
|
+
docket: `E0100${String(i + 24).padStart(2, '0')}`,
|
|
2190
|
+
custDocket: i % 3 === 0 ? `CD-${i + 100}` : '',
|
|
2191
|
+
uom: ['Tonne', 'Load', 'Hour'][i % 3],
|
|
2192
|
+
qty,
|
|
2193
|
+
rate,
|
|
2194
|
+
totalPre,
|
|
2195
|
+
totalPost: Math.round(totalPre * 1.1 * 100) / 100,
|
|
2196
|
+
tipName: ['Pejar Tip', 'Brookvale Tip', 'Taren Point Tip'][i % 3],
|
|
2197
|
+
tipUom: ['Tonne', 'Load'][i % 2],
|
|
2198
|
+
tipQty,
|
|
2199
|
+
tipRate,
|
|
2200
|
+
tipPre,
|
|
2201
|
+
tipPost: Math.round(tipPre * 1.1 * 100) / 100,
|
|
2202
|
+
tipInvoice: i % 4 === 0 ? `TIP-${i + 200}` : '',
|
|
2203
|
+
truckPre: Math.round((500 + Math.random() * 1500) * 100) / 100,
|
|
2204
|
+
truckPost: Math.round((550 + Math.random() * 1650) * 100) / 100,
|
|
2205
|
+
subRate,
|
|
2206
|
+
subPre,
|
|
2207
|
+
subPost: Math.round(subPre * 1.1 * 100) / 100,
|
|
2208
|
+
subInvoice: i % 5 === 0 ? `SUB-${i + 300}` : '',
|
|
2209
|
+
weekEnding: `2026-03-${String(Math.min(28, ((Math.floor(i / 7) + 1) * 7))).padStart(2, '0')}`,
|
|
2210
|
+
profit,
|
|
2211
|
+
};
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
const docketColumns: SpreadsheetColumn<DocketRecord>[] = [
|
|
2215
|
+
{ id: 'rego', label: 'Rego', width: 85, sortable: true, filterable: true, editable: true },
|
|
2216
|
+
{ id: 'driver', label: 'Driver', width: 160, sortable: true, filterable: true },
|
|
2217
|
+
{ id: 'type', label: 'Type', width: 70, sortable: true, filterable: true, editable: true, type: 'select', options: ['LNG', 'SUBBY'] },
|
|
2218
|
+
{ id: 'date', label: 'Date', width: 100, sortable: true, filterable: true, type: 'date' },
|
|
2219
|
+
{ id: 'job', label: 'Job', width: 180, sortable: true, filterable: true },
|
|
2220
|
+
{ id: 'customer', label: 'Customer', width: 180, sortable: true, filterable: true },
|
|
2221
|
+
{ id: 'custInv', label: 'Cust Inv#', width: 100, sortable: true, filterable: true, editable: true },
|
|
2222
|
+
{ id: 'from', label: 'From', width: 200, sortable: true, filterable: true, editable: true },
|
|
2223
|
+
{ id: 'tipSite', label: 'Tip Site', width: 200, sortable: true, filterable: true, editable: true },
|
|
2224
|
+
{ id: 'comment', label: 'Comment', width: 150, sortable: true, filterable: true, editable: true },
|
|
2225
|
+
{ id: 'docket', label: 'Docket', width: 90, sortable: true, filterable: true },
|
|
2226
|
+
{ id: 'custDocket', label: 'Cust Docket', width: 100, sortable: true, filterable: true, editable: true },
|
|
2227
|
+
{ id: 'uom', label: 'UoM', width: 70, editable: true, type: 'select', options: ['Tonne', 'Load', 'Hour'] },
|
|
2228
|
+
{ id: 'qty', label: 'Qty', width: 70, sortable: true, type: 'number', align: 'right', editable: true },
|
|
2229
|
+
{ id: 'rate', label: 'Rate', width: 70, sortable: true, type: 'number', align: 'right', editable: true },
|
|
2230
|
+
{ id: 'totalPre', label: 'Total Pre', width: 85, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2231
|
+
{ id: 'totalPost', label: 'Total Post', width: 85, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2232
|
+
{ id: 'tipName', label: 'Tip Name', width: 140, sortable: true, filterable: true, editable: true },
|
|
2233
|
+
{ id: 'tipUom', label: 'Tip UoM', width: 70, editable: true, type: 'select', options: ['Tonne', 'Load'] },
|
|
2234
|
+
{ id: 'tipQty', label: 'Tip Qty', width: 70, sortable: true, filterable: true, type: 'number', align: 'right', editable: true },
|
|
2235
|
+
{ id: 'tipRate', label: 'Tip Rate', width: 70, sortable: true, filterable: true, type: 'number', align: 'right', editable: true },
|
|
2236
|
+
{ id: 'tipPre', label: 'Tip Pre', width: 80, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2237
|
+
{ id: 'tipPost', label: 'Tip Post', width: 80, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2238
|
+
{ id: 'tipInvoice', label: 'Tip Invoice', width: 100, sortable: true, filterable: true, editable: true },
|
|
2239
|
+
{ id: 'truckPre', label: 'Truck Pre', width: 85, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2240
|
+
{ id: 'truckPost', label: 'Truck Post', width: 85, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2241
|
+
{ id: 'subRate', label: 'Sub Rate', width: 80, sortable: true, filterable: true, type: 'number', align: 'right', editable: true },
|
|
2242
|
+
{ id: 'subPre', label: 'Sub Pre', width: 80, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2243
|
+
{ id: 'subPost', label: 'Sub Post', width: 80, type: 'number', align: 'right', render: (v) => `$${Number(v).toFixed(2)}` },
|
|
2244
|
+
{ id: 'subInvoice', label: 'Sub Invoice', width: 100, sortable: true, filterable: true, editable: true },
|
|
2245
|
+
{ id: 'weekEnding', label: 'Week Ending', width: 100, sortable: true },
|
|
2246
|
+
{
|
|
2247
|
+
id: 'profit',
|
|
2248
|
+
label: 'Profit',
|
|
2249
|
+
width: 90,
|
|
2250
|
+
type: 'number',
|
|
2251
|
+
align: 'right',
|
|
2252
|
+
sortable: true,
|
|
2253
|
+
render: (v) => {
|
|
2254
|
+
const num = Number(v);
|
|
2255
|
+
const color = num < 0 ? 'text-red-600' : 'text-green-600';
|
|
2256
|
+
return <span className={`font-medium ${color}`}>${num.toFixed(2)}</span>;
|
|
2257
|
+
},
|
|
2258
|
+
},
|
|
2259
|
+
];
|
|
2260
|
+
|
|
2261
|
+
const docketColumnGroups: SpreadsheetColumnGroup[] = [
|
|
2262
|
+
{
|
|
2263
|
+
id: 'customer-billing',
|
|
2264
|
+
label: 'Customer & Billing',
|
|
2265
|
+
columns: ['rego', 'driver', 'type', 'date', 'job', 'customer', 'custInv', 'from', 'tipSite', 'comment', 'docket', 'custDocket', 'uom', 'qty', 'rate', 'totalPre', 'totalPost'],
|
|
2266
|
+
headerColor: '#93C5FD',
|
|
2267
|
+
collapsible: true,
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
id: 'tip-site',
|
|
2271
|
+
label: 'Tip Site',
|
|
2272
|
+
columns: ['tipName', 'tipUom', 'tipQty', 'tipRate', 'tipPre', 'tipPost', 'tipInvoice'],
|
|
2273
|
+
headerColor: '#86EFAC',
|
|
2274
|
+
collapsible: true,
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
id: 'truck-hire',
|
|
2278
|
+
label: 'Truck Hire',
|
|
2279
|
+
columns: ['truckPre', 'truckPost'],
|
|
2280
|
+
headerColor: '#FDBA74',
|
|
2281
|
+
collapsible: true,
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
id: 'subcontractor',
|
|
2285
|
+
label: 'Subcontractor',
|
|
2286
|
+
columns: ['subRate', 'subPre', 'subPost', 'subInvoice'],
|
|
2287
|
+
headerColor: '#D8B4FE',
|
|
2288
|
+
collapsible: true,
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
id: 'summary',
|
|
2292
|
+
label: 'Summary',
|
|
2293
|
+
columns: ['weekEnding', 'profit'],
|
|
2294
|
+
headerColor: '#D1D5DB',
|
|
2295
|
+
collapsible: true,
|
|
2296
|
+
},
|
|
2297
|
+
];
|
|
2298
|
+
|
|
2299
|
+
const COMPREHENSIVE_STORAGE_KEY = 'storybook-comprehensive-test-settings';
|
|
2300
|
+
|
|
2301
|
+
const getStoredComprehensiveSettings = () => {
|
|
2302
|
+
try {
|
|
2303
|
+
const stored = localStorage.getItem(COMPREHENSIVE_STORAGE_KEY);
|
|
2304
|
+
return stored ? JSON.parse(stored) : null;
|
|
2305
|
+
} catch { return null; }
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
export const ComprehensiveTest: Story = {
|
|
2309
|
+
render: () => {
|
|
2310
|
+
const [data, setData] = useState(docketData);
|
|
2311
|
+
const storedSettings = getStoredComprehensiveSettings();
|
|
2312
|
+
|
|
2313
|
+
const handleCellsEdit = (edits: CellEdit[]) => {
|
|
2314
|
+
setData((prev) =>
|
|
2315
|
+
prev.map((row) => {
|
|
2316
|
+
const editsForRow = edits.filter((e) => e.rowId === row.id);
|
|
2317
|
+
if (editsForRow.length > 0) {
|
|
2318
|
+
const updates = editsForRow.reduce(
|
|
2319
|
+
(acc, edit) => ({ ...acc, [edit.columnId]: edit.value }),
|
|
2320
|
+
{}
|
|
2321
|
+
);
|
|
2322
|
+
return { ...row, ...updates };
|
|
2323
|
+
}
|
|
2324
|
+
return row;
|
|
2325
|
+
})
|
|
2326
|
+
);
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
return (
|
|
2330
|
+
<div className="flex flex-col h-screen">
|
|
2331
|
+
<div className="p-3 bg-blue-50 border-b border-blue-200 shrink-0">
|
|
2332
|
+
<h3 className="font-semibold text-blue-900 mb-1">
|
|
2333
|
+
Comprehensive Test — Docket Management
|
|
2334
|
+
</h3>
|
|
2335
|
+
<p className="text-xs text-blue-700">
|
|
2336
|
+
32 columns, 5 groups, 200 rows. Double-click to edit | Shift+Click for range | Cmd+A select all | Click header to select column | Sort via icon | Enter to submit filter | Resize columns by dragging header edges
|
|
2337
|
+
</p>
|
|
2338
|
+
</div>
|
|
2339
|
+
|
|
2340
|
+
<div className="flex-1 min-h-0 p-2">
|
|
2341
|
+
<Spreadsheet
|
|
2342
|
+
data={data}
|
|
2343
|
+
columns={docketColumns}
|
|
2344
|
+
columnGroups={docketColumnGroups}
|
|
2345
|
+
getRowId={(row) => row.id}
|
|
2346
|
+
onCellsEdit={handleCellsEdit}
|
|
2347
|
+
showToolbar
|
|
2348
|
+
showPagination
|
|
2349
|
+
enableRowSelection
|
|
2350
|
+
enableCellEditing
|
|
2351
|
+
enableHighlighting
|
|
2352
|
+
enableComments
|
|
2353
|
+
enableUndoRedo
|
|
2354
|
+
settings={{
|
|
2355
|
+
defaultPinnedColumns: storedSettings?.defaultPinnedColumns ?? ['__row_index__'],
|
|
2356
|
+
defaultPageSize: storedSettings?.defaultPageSize ?? 100,
|
|
2357
|
+
defaultZoom: storedSettings?.defaultZoom ?? 100,
|
|
2358
|
+
autoSave: storedSettings?.autoSave ?? true,
|
|
2359
|
+
compactView: storedSettings?.compactView ?? false,
|
|
2360
|
+
columnWidths: storedSettings?.columnWidths,
|
|
2361
|
+
}}
|
|
2362
|
+
onSettingsChange={(settings) => {
|
|
2363
|
+
localStorage.setItem(COMPREHENSIVE_STORAGE_KEY, JSON.stringify(settings));
|
|
2364
|
+
}}
|
|
2365
|
+
/>
|
|
2366
|
+
</div>
|
|
2367
|
+
</div>
|
|
2368
|
+
);
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
|
|
2118
2372
|
export const WithRightPinnedColumns: Story = {
|
|
2119
2373
|
render: () => {
|
|
2120
2374
|
const [data, setData] = useState(sampleUsers);
|