@xcelsior/ui-spreadsheets 1.1.13 → 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/dist/index.mjs CHANGED
@@ -145,7 +145,162 @@ function cn(...inputs) {
145
145
  }
146
146
 
147
147
  // src/components/SpreadsheetCell.tsx
148
- import { useState, useRef, useEffect, memo } from "react";
148
+ import { useState as useState2, useRef, useEffect, memo } from "react";
149
+
150
+ // src/hooks/useSpreadsheetPinning.ts
151
+ import { useCallback, useMemo, useState } from "react";
152
+ var ROW_INDEX_COLUMN_ID = "__row_index__";
153
+ var ROW_INDEX_COLUMN_WIDTH = 80;
154
+ var MIN_PINNED_COLUMN_WIDTH = 100;
155
+ function useSpreadsheetPinning({
156
+ columns,
157
+ columnGroups,
158
+ showRowIndex = true,
159
+ defaultPinnedColumns = [],
160
+ defaultPinnedRightColumns = []
161
+ }) {
162
+ const [pinnedColumns, setPinnedColumns] = useState(() => {
163
+ const map = /* @__PURE__ */ new Map();
164
+ defaultPinnedColumns.forEach((col) => {
165
+ map.set(col, "left");
166
+ });
167
+ defaultPinnedRightColumns.forEach((col) => {
168
+ map.set(col, "right");
169
+ });
170
+ return map;
171
+ });
172
+ const [collapsedGroups, setCollapsedGroups] = useState(/* @__PURE__ */ new Set());
173
+ const isRowIndexPinned = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
174
+ const handleTogglePin = useCallback((columnId) => {
175
+ setPinnedColumns((prev) => {
176
+ const newMap = new Map(prev);
177
+ if (newMap.has(columnId)) {
178
+ newMap.delete(columnId);
179
+ } else {
180
+ newMap.set(columnId, "left");
181
+ }
182
+ return newMap;
183
+ });
184
+ }, []);
185
+ const setPinnedColumnsFromIds = useCallback((columnIds) => {
186
+ const map = /* @__PURE__ */ new Map();
187
+ columnIds.forEach((col) => {
188
+ map.set(col, "left");
189
+ });
190
+ setPinnedColumns(map);
191
+ }, []);
192
+ const handleToggleGroupCollapse = useCallback((groupId) => {
193
+ setCollapsedGroups((prev) => {
194
+ const newSet = new Set(prev);
195
+ if (newSet.has(groupId)) {
196
+ newSet.delete(groupId);
197
+ } else {
198
+ newSet.add(groupId);
199
+ }
200
+ return newSet;
201
+ });
202
+ }, []);
203
+ const visibleColumns = useMemo(() => {
204
+ if (!columns || !Array.isArray(columns) || columns.length === 0) {
205
+ return [];
206
+ }
207
+ let result = [...columns];
208
+ if (columnGroups && Array.isArray(columnGroups)) {
209
+ result = result.filter((column) => {
210
+ const group = columnGroups.find((g) => g.columns.includes(column.id));
211
+ if (!group) return true;
212
+ if (!collapsedGroups.has(group.id)) return true;
213
+ return pinnedColumns.has(column.id);
214
+ });
215
+ }
216
+ const nonRowIndexPinned = Array.from(pinnedColumns.keys()).filter(
217
+ (id) => id !== ROW_INDEX_COLUMN_ID
218
+ );
219
+ if (nonRowIndexPinned.length === 0) {
220
+ return result;
221
+ }
222
+ const leftPinned = [];
223
+ const unpinned = [];
224
+ const rightPinned = [];
225
+ const pinnedLeftIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
226
+ const pinnedRightIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "right" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
227
+ for (const column of result) {
228
+ const pinSide = pinnedColumns.get(column.id);
229
+ if (pinSide === "left") {
230
+ leftPinned.push(column);
231
+ } else if (pinSide === "right") {
232
+ rightPinned.push(column);
233
+ } else {
234
+ unpinned.push(column);
235
+ }
236
+ }
237
+ leftPinned.sort((a, b) => pinnedLeftIds.indexOf(a.id) - pinnedLeftIds.indexOf(b.id));
238
+ rightPinned.sort((a, b) => pinnedRightIds.indexOf(a.id) - pinnedRightIds.indexOf(b.id));
239
+ return [...leftPinned, ...unpinned, ...rightPinned];
240
+ }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
241
+ const getColumnLeftOffset = useCallback(
242
+ (columnId) => {
243
+ if (columnId === ROW_INDEX_COLUMN_ID) {
244
+ return 0;
245
+ }
246
+ const pinnedLeft = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
247
+ const index = pinnedLeft.indexOf(columnId);
248
+ const isRowIndexPinnedNow = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
249
+ const baseOffset = showRowIndex && isRowIndexPinnedNow ? ROW_INDEX_COLUMN_WIDTH : 0;
250
+ if (index === -1) return baseOffset;
251
+ let offset = baseOffset;
252
+ for (let i = 0; i < index; i++) {
253
+ const col = columns.find((c) => c.id === pinnedLeft[i]);
254
+ const configuredWidth = col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH;
255
+ offset += Math.max(configuredWidth, MIN_PINNED_COLUMN_WIDTH);
256
+ }
257
+ return offset;
258
+ },
259
+ [pinnedColumns, columns, showRowIndex]
260
+ );
261
+ const isColumnPinned = useCallback(
262
+ (columnId) => {
263
+ return pinnedColumns.has(columnId);
264
+ },
265
+ [pinnedColumns]
266
+ );
267
+ const getColumnPinSide = useCallback(
268
+ (columnId) => {
269
+ return pinnedColumns.get(columnId);
270
+ },
271
+ [pinnedColumns]
272
+ );
273
+ const getColumnRightOffset = useCallback(
274
+ (columnId) => {
275
+ const pinnedRight = Array.from(pinnedColumns.entries()).filter(([, side]) => side === "right").map(([id]) => id);
276
+ const index = pinnedRight.indexOf(columnId);
277
+ if (index === -1) return 0;
278
+ let offset = 0;
279
+ for (let i = pinnedRight.length - 1; i > index; i--) {
280
+ const col = columns.find((c) => c.id === pinnedRight[i]);
281
+ const configuredWidth = col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH;
282
+ offset += Math.max(configuredWidth, MIN_PINNED_COLUMN_WIDTH);
283
+ }
284
+ return offset;
285
+ },
286
+ [pinnedColumns, columns]
287
+ );
288
+ return {
289
+ pinnedColumns,
290
+ isRowIndexPinned,
291
+ collapsedGroups,
292
+ visibleColumns,
293
+ handleTogglePin,
294
+ handleToggleGroupCollapse,
295
+ setPinnedColumnsFromIds,
296
+ getColumnLeftOffset,
297
+ getColumnRightOffset,
298
+ isColumnPinned,
299
+ getColumnPinSide
300
+ };
301
+ }
302
+
303
+ // src/components/SpreadsheetCell.tsx
149
304
  import { jsx, jsxs } from "react/jsx-runtime";
150
305
  var cellPaddingCompact = "px-1 py-px";
151
306
  var cellPaddingNormal = "px-2 py-1";
@@ -182,7 +337,7 @@ var SpreadsheetCell = ({
182
337
  onViewComments,
183
338
  className
184
339
  }) => {
185
- const [localValue, setLocalValue] = useState(value);
340
+ const [localValue, setLocalValue] = useState2(value);
186
341
  const inputRef = useRef(null);
187
342
  const selectRef = useRef(null);
188
343
  useEffect(() => {
@@ -361,6 +516,12 @@ var SpreadsheetCell = ({
361
516
  style: {
362
517
  backgroundColor: isInSelection ? "rgb(239 246 255)" : getBackgroundColor(),
363
518
  minWidth: column.minWidth || column.width,
519
+ // Pinned columns must have a fixed width so sticky offsets stay correct.
520
+ // Enforce MIN_PINNED_COLUMN_WIDTH so header actions always fit.
521
+ ...isPinned && {
522
+ width: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
523
+ maxWidth: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH)
524
+ },
364
525
  ...positionStyles,
365
526
  ...selectionBorderStyles
366
527
  },
@@ -461,7 +622,7 @@ var MemoizedSpreadsheetCell = memo(SpreadsheetCell, (prevProps, nextProps) => {
461
622
  MemoizedSpreadsheetCell.displayName = "MemoizedSpreadsheetCell";
462
623
 
463
624
  // src/components/SpreadsheetFilterDropdown.tsx
464
- import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
625
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
465
626
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
466
627
  var TEXT_OPERATORS = [
467
628
  { value: "contains", label: "Contains" },
@@ -507,24 +668,24 @@ var SpreadsheetFilterDropdown = ({
507
668
  onClose,
508
669
  className
509
670
  }) => {
510
- const [textOperator, setTextOperator] = useState2(
671
+ const [textOperator, setTextOperator] = useState3(
511
672
  filter?.textCondition?.operator || "contains"
512
673
  );
513
- const [textValue, setTextValue] = useState2(filter?.textCondition?.value || "");
514
- const [numberOperator, setNumberOperator] = useState2(
674
+ const [textValue, setTextValue] = useState3(filter?.textCondition?.value || "");
675
+ const [numberOperator, setNumberOperator] = useState3(
515
676
  filter?.numberCondition?.operator || "equals"
516
677
  );
517
- const [numberValue, setNumberValue] = useState2(
678
+ const [numberValue, setNumberValue] = useState3(
518
679
  filter?.numberCondition?.value?.toString() || ""
519
680
  );
520
- const [numberValueTo, setNumberValueTo] = useState2(
681
+ const [numberValueTo, setNumberValueTo] = useState3(
521
682
  filter?.numberCondition?.valueTo?.toString() || ""
522
683
  );
523
- const [dateOperator, setDateOperator] = useState2(
684
+ const [dateOperator, setDateOperator] = useState3(
524
685
  filter?.dateCondition?.operator || "equals"
525
686
  );
526
- const [dateValue, setDateValue] = useState2(filter?.dateCondition?.value || "");
527
- const [dateValueTo, setDateValueTo] = useState2(filter?.dateCondition?.valueTo || "");
687
+ const [dateValue, setDateValue] = useState3(filter?.dateCondition?.value || "");
688
+ const [dateValueTo, setDateValueTo] = useState3(filter?.dateCondition?.valueTo || "");
528
689
  const dropdownRef = useRef2(null);
529
690
  const isNumeric = column.type === "number";
530
691
  const isDate = column.type === "date";
@@ -1202,6 +1363,12 @@ var SpreadsheetHeader = ({
1202
1363
  backgroundColor: highlightColor || "rgb(243 244 246)",
1203
1364
  // gray-100
1204
1365
  minWidth: column.minWidth || column.width,
1366
+ // Pinned columns must have a fixed width so sticky offsets stay correct.
1367
+ // Enforce MIN_PINNED_COLUMN_WIDTH so header actions (pin/filter/highlight) always fit.
1368
+ ...isPinned && {
1369
+ width: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
1370
+ maxWidth: Math.max(column.minWidth || column.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH)
1371
+ },
1205
1372
  top: 0,
1206
1373
  // For sticky header
1207
1374
  ...positionStyles
@@ -1234,156 +1401,6 @@ var SpreadsheetHeader = ({
1234
1401
  };
1235
1402
  SpreadsheetHeader.displayName = "SpreadsheetHeader";
1236
1403
 
1237
- // src/hooks/useSpreadsheetPinning.ts
1238
- import { useCallback, useMemo, useState as useState3 } from "react";
1239
- var ROW_INDEX_COLUMN_ID = "__row_index__";
1240
- var ROW_INDEX_COLUMN_WIDTH = 80;
1241
- function useSpreadsheetPinning({
1242
- columns,
1243
- columnGroups,
1244
- showRowIndex = true,
1245
- defaultPinnedColumns = [],
1246
- defaultPinnedRightColumns = []
1247
- }) {
1248
- const [pinnedColumns, setPinnedColumns] = useState3(() => {
1249
- const map = /* @__PURE__ */ new Map();
1250
- defaultPinnedColumns.forEach((col) => {
1251
- map.set(col, "left");
1252
- });
1253
- defaultPinnedRightColumns.forEach((col) => {
1254
- map.set(col, "right");
1255
- });
1256
- return map;
1257
- });
1258
- const [collapsedGroups, setCollapsedGroups] = useState3(/* @__PURE__ */ new Set());
1259
- const isRowIndexPinned = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
1260
- const handleTogglePin = useCallback((columnId) => {
1261
- setPinnedColumns((prev) => {
1262
- const newMap = new Map(prev);
1263
- if (newMap.has(columnId)) {
1264
- newMap.delete(columnId);
1265
- } else {
1266
- newMap.set(columnId, "left");
1267
- }
1268
- return newMap;
1269
- });
1270
- }, []);
1271
- const setPinnedColumnsFromIds = useCallback((columnIds) => {
1272
- const map = /* @__PURE__ */ new Map();
1273
- columnIds.forEach((col) => {
1274
- map.set(col, "left");
1275
- });
1276
- setPinnedColumns(map);
1277
- }, []);
1278
- const handleToggleGroupCollapse = useCallback((groupId) => {
1279
- setCollapsedGroups((prev) => {
1280
- const newSet = new Set(prev);
1281
- if (newSet.has(groupId)) {
1282
- newSet.delete(groupId);
1283
- } else {
1284
- newSet.add(groupId);
1285
- }
1286
- return newSet;
1287
- });
1288
- }, []);
1289
- const visibleColumns = useMemo(() => {
1290
- if (!columns || !Array.isArray(columns) || columns.length === 0) {
1291
- return [];
1292
- }
1293
- let result = [...columns];
1294
- if (columnGroups && Array.isArray(columnGroups)) {
1295
- result = result.filter((column) => {
1296
- const group = columnGroups.find((g) => g.columns.includes(column.id));
1297
- if (!group) return true;
1298
- if (!collapsedGroups.has(group.id)) return true;
1299
- return pinnedColumns.has(column.id);
1300
- });
1301
- }
1302
- const nonRowIndexPinned = Array.from(pinnedColumns.keys()).filter(
1303
- (id) => id !== ROW_INDEX_COLUMN_ID
1304
- );
1305
- if (nonRowIndexPinned.length === 0) {
1306
- return result;
1307
- }
1308
- const leftPinned = [];
1309
- const unpinned = [];
1310
- const rightPinned = [];
1311
- const pinnedLeftIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
1312
- const pinnedRightIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "right" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
1313
- for (const column of result) {
1314
- const pinSide = pinnedColumns.get(column.id);
1315
- if (pinSide === "left") {
1316
- leftPinned.push(column);
1317
- } else if (pinSide === "right") {
1318
- rightPinned.push(column);
1319
- } else {
1320
- unpinned.push(column);
1321
- }
1322
- }
1323
- leftPinned.sort((a, b) => pinnedLeftIds.indexOf(a.id) - pinnedLeftIds.indexOf(b.id));
1324
- rightPinned.sort((a, b) => pinnedRightIds.indexOf(a.id) - pinnedRightIds.indexOf(b.id));
1325
- return [...leftPinned, ...unpinned, ...rightPinned];
1326
- }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
1327
- const getColumnLeftOffset = useCallback(
1328
- (columnId) => {
1329
- if (columnId === ROW_INDEX_COLUMN_ID) {
1330
- return 0;
1331
- }
1332
- const pinnedLeft = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
1333
- const index = pinnedLeft.indexOf(columnId);
1334
- const isRowIndexPinnedNow = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
1335
- const baseOffset = showRowIndex && isRowIndexPinnedNow ? ROW_INDEX_COLUMN_WIDTH : 0;
1336
- if (index === -1) return baseOffset;
1337
- let offset = baseOffset;
1338
- for (let i = 0; i < index; i++) {
1339
- const col = columns.find((c) => c.id === pinnedLeft[i]);
1340
- offset += col?.minWidth || col?.width || 100;
1341
- }
1342
- return offset;
1343
- },
1344
- [pinnedColumns, columns, showRowIndex]
1345
- );
1346
- const isColumnPinned = useCallback(
1347
- (columnId) => {
1348
- return pinnedColumns.has(columnId);
1349
- },
1350
- [pinnedColumns]
1351
- );
1352
- const getColumnPinSide = useCallback(
1353
- (columnId) => {
1354
- return pinnedColumns.get(columnId);
1355
- },
1356
- [pinnedColumns]
1357
- );
1358
- const getColumnRightOffset = useCallback(
1359
- (columnId) => {
1360
- const pinnedRight = Array.from(pinnedColumns.entries()).filter(([, side]) => side === "right").map(([id]) => id);
1361
- const index = pinnedRight.indexOf(columnId);
1362
- if (index === -1) return 0;
1363
- let offset = 0;
1364
- for (let i = pinnedRight.length - 1; i > index; i--) {
1365
- const col = columns.find((c) => c.id === pinnedRight[i]);
1366
- offset += col?.minWidth || col?.width || 100;
1367
- }
1368
- return offset;
1369
- },
1370
- [pinnedColumns, columns]
1371
- );
1372
- return {
1373
- pinnedColumns,
1374
- isRowIndexPinned,
1375
- collapsedGroups,
1376
- visibleColumns,
1377
- handleTogglePin,
1378
- handleToggleGroupCollapse,
1379
- setPinnedColumnsFromIds,
1380
- getColumnLeftOffset,
1381
- getColumnRightOffset,
1382
- isColumnPinned,
1383
- getColumnPinSide
1384
- };
1385
- }
1386
-
1387
1404
  // src/components/RowIndexColumnHeader.tsx
1388
1405
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1389
1406
  var cellPaddingCompact3 = "px-1 py-0.5";
@@ -4010,7 +4027,9 @@ function Spreadsheet({
4010
4027
  position: "sticky",
4011
4028
  left: isPinnedLeft ? `${getColumnLeftOffset(item.columnId)}px` : void 0,
4012
4029
  right: !isPinnedLeft ? `${getColumnRightOffset(item.columnId)}px` : void 0,
4013
- minWidth: col?.minWidth || col?.width
4030
+ minWidth: Math.max(col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
4031
+ width: Math.max(col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH),
4032
+ maxWidth: Math.max(col?.minWidth || col?.width || MIN_PINNED_COLUMN_WIDTH, MIN_PINNED_COLUMN_WIDTH)
4014
4033
  }
4015
4034
  },
4016
4035
  `pinned-group-${item.columnId}`