@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/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,10 +516,11 @@ var SpreadsheetCell = ({
361
516
  style: {
362
517
  backgroundColor: isInSelection ? "rgb(239 246 255)" : getBackgroundColor(),
363
518
  minWidth: column.minWidth || column.width,
364
- // Pinned columns need fixed width so sticky offset calculations are accurate
519
+ // Pinned columns must have a fixed width so sticky offsets stay correct.
520
+ // Enforce MIN_PINNED_COLUMN_WIDTH so header actions always fit.
365
521
  ...isPinned && {
366
- width: column.minWidth || column.width,
367
- maxWidth: column.minWidth || column.width
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)
368
524
  },
369
525
  ...positionStyles,
370
526
  ...selectionBorderStyles
@@ -466,7 +622,7 @@ var MemoizedSpreadsheetCell = memo(SpreadsheetCell, (prevProps, nextProps) => {
466
622
  MemoizedSpreadsheetCell.displayName = "MemoizedSpreadsheetCell";
467
623
 
468
624
  // src/components/SpreadsheetFilterDropdown.tsx
469
- 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";
470
626
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
471
627
  var TEXT_OPERATORS = [
472
628
  { value: "contains", label: "Contains" },
@@ -512,24 +668,24 @@ var SpreadsheetFilterDropdown = ({
512
668
  onClose,
513
669
  className
514
670
  }) => {
515
- const [textOperator, setTextOperator] = useState2(
671
+ const [textOperator, setTextOperator] = useState3(
516
672
  filter?.textCondition?.operator || "contains"
517
673
  );
518
- const [textValue, setTextValue] = useState2(filter?.textCondition?.value || "");
519
- const [numberOperator, setNumberOperator] = useState2(
674
+ const [textValue, setTextValue] = useState3(filter?.textCondition?.value || "");
675
+ const [numberOperator, setNumberOperator] = useState3(
520
676
  filter?.numberCondition?.operator || "equals"
521
677
  );
522
- const [numberValue, setNumberValue] = useState2(
678
+ const [numberValue, setNumberValue] = useState3(
523
679
  filter?.numberCondition?.value?.toString() || ""
524
680
  );
525
- const [numberValueTo, setNumberValueTo] = useState2(
681
+ const [numberValueTo, setNumberValueTo] = useState3(
526
682
  filter?.numberCondition?.valueTo?.toString() || ""
527
683
  );
528
- const [dateOperator, setDateOperator] = useState2(
684
+ const [dateOperator, setDateOperator] = useState3(
529
685
  filter?.dateCondition?.operator || "equals"
530
686
  );
531
- const [dateValue, setDateValue] = useState2(filter?.dateCondition?.value || "");
532
- const [dateValueTo, setDateValueTo] = useState2(filter?.dateCondition?.valueTo || "");
687
+ const [dateValue, setDateValue] = useState3(filter?.dateCondition?.value || "");
688
+ const [dateValueTo, setDateValueTo] = useState3(filter?.dateCondition?.valueTo || "");
533
689
  const dropdownRef = useRef2(null);
534
690
  const isNumeric = column.type === "number";
535
691
  const isDate = column.type === "date";
@@ -1207,10 +1363,11 @@ var SpreadsheetHeader = ({
1207
1363
  backgroundColor: highlightColor || "rgb(243 244 246)",
1208
1364
  // gray-100
1209
1365
  minWidth: column.minWidth || column.width,
1210
- // Pinned columns need fixed width so sticky offset calculations are accurate
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.
1211
1368
  ...isPinned && {
1212
- width: column.minWidth || column.width,
1213
- maxWidth: column.minWidth || column.width
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)
1214
1371
  },
1215
1372
  top: 0,
1216
1373
  // For sticky header
@@ -1244,156 +1401,6 @@ var SpreadsheetHeader = ({
1244
1401
  };
1245
1402
  SpreadsheetHeader.displayName = "SpreadsheetHeader";
1246
1403
 
1247
- // src/hooks/useSpreadsheetPinning.ts
1248
- import { useCallback, useMemo, useState as useState3 } from "react";
1249
- var ROW_INDEX_COLUMN_ID = "__row_index__";
1250
- var ROW_INDEX_COLUMN_WIDTH = 80;
1251
- function useSpreadsheetPinning({
1252
- columns,
1253
- columnGroups,
1254
- showRowIndex = true,
1255
- defaultPinnedColumns = [],
1256
- defaultPinnedRightColumns = []
1257
- }) {
1258
- const [pinnedColumns, setPinnedColumns] = useState3(() => {
1259
- const map = /* @__PURE__ */ new Map();
1260
- defaultPinnedColumns.forEach((col) => {
1261
- map.set(col, "left");
1262
- });
1263
- defaultPinnedRightColumns.forEach((col) => {
1264
- map.set(col, "right");
1265
- });
1266
- return map;
1267
- });
1268
- const [collapsedGroups, setCollapsedGroups] = useState3(/* @__PURE__ */ new Set());
1269
- const isRowIndexPinned = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
1270
- const handleTogglePin = useCallback((columnId) => {
1271
- setPinnedColumns((prev) => {
1272
- const newMap = new Map(prev);
1273
- if (newMap.has(columnId)) {
1274
- newMap.delete(columnId);
1275
- } else {
1276
- newMap.set(columnId, "left");
1277
- }
1278
- return newMap;
1279
- });
1280
- }, []);
1281
- const setPinnedColumnsFromIds = useCallback((columnIds) => {
1282
- const map = /* @__PURE__ */ new Map();
1283
- columnIds.forEach((col) => {
1284
- map.set(col, "left");
1285
- });
1286
- setPinnedColumns(map);
1287
- }, []);
1288
- const handleToggleGroupCollapse = useCallback((groupId) => {
1289
- setCollapsedGroups((prev) => {
1290
- const newSet = new Set(prev);
1291
- if (newSet.has(groupId)) {
1292
- newSet.delete(groupId);
1293
- } else {
1294
- newSet.add(groupId);
1295
- }
1296
- return newSet;
1297
- });
1298
- }, []);
1299
- const visibleColumns = useMemo(() => {
1300
- if (!columns || !Array.isArray(columns) || columns.length === 0) {
1301
- return [];
1302
- }
1303
- let result = [...columns];
1304
- if (columnGroups && Array.isArray(columnGroups)) {
1305
- result = result.filter((column) => {
1306
- const group = columnGroups.find((g) => g.columns.includes(column.id));
1307
- if (!group) return true;
1308
- if (!collapsedGroups.has(group.id)) return true;
1309
- return pinnedColumns.has(column.id);
1310
- });
1311
- }
1312
- const nonRowIndexPinned = Array.from(pinnedColumns.keys()).filter(
1313
- (id) => id !== ROW_INDEX_COLUMN_ID
1314
- );
1315
- if (nonRowIndexPinned.length === 0) {
1316
- return result;
1317
- }
1318
- const leftPinned = [];
1319
- const unpinned = [];
1320
- const rightPinned = [];
1321
- const pinnedLeftIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
1322
- const pinnedRightIds = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "right" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
1323
- for (const column of result) {
1324
- const pinSide = pinnedColumns.get(column.id);
1325
- if (pinSide === "left") {
1326
- leftPinned.push(column);
1327
- } else if (pinSide === "right") {
1328
- rightPinned.push(column);
1329
- } else {
1330
- unpinned.push(column);
1331
- }
1332
- }
1333
- leftPinned.sort((a, b) => pinnedLeftIds.indexOf(a.id) - pinnedLeftIds.indexOf(b.id));
1334
- rightPinned.sort((a, b) => pinnedRightIds.indexOf(a.id) - pinnedRightIds.indexOf(b.id));
1335
- return [...leftPinned, ...unpinned, ...rightPinned];
1336
- }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
1337
- const getColumnLeftOffset = useCallback(
1338
- (columnId) => {
1339
- if (columnId === ROW_INDEX_COLUMN_ID) {
1340
- return 0;
1341
- }
1342
- const pinnedLeft = Array.from(pinnedColumns.entries()).filter(([id, side]) => side === "left" && id !== ROW_INDEX_COLUMN_ID).map(([id]) => id);
1343
- const index = pinnedLeft.indexOf(columnId);
1344
- const isRowIndexPinnedNow = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
1345
- const baseOffset = showRowIndex && isRowIndexPinnedNow ? ROW_INDEX_COLUMN_WIDTH : 0;
1346
- if (index === -1) return baseOffset;
1347
- let offset = baseOffset;
1348
- for (let i = 0; i < index; i++) {
1349
- const col = columns.find((c) => c.id === pinnedLeft[i]);
1350
- offset += col?.minWidth || col?.width || 100;
1351
- }
1352
- return offset;
1353
- },
1354
- [pinnedColumns, columns, showRowIndex]
1355
- );
1356
- const isColumnPinned = useCallback(
1357
- (columnId) => {
1358
- return pinnedColumns.has(columnId);
1359
- },
1360
- [pinnedColumns]
1361
- );
1362
- const getColumnPinSide = useCallback(
1363
- (columnId) => {
1364
- return pinnedColumns.get(columnId);
1365
- },
1366
- [pinnedColumns]
1367
- );
1368
- const getColumnRightOffset = useCallback(
1369
- (columnId) => {
1370
- const pinnedRight = Array.from(pinnedColumns.entries()).filter(([, side]) => side === "right").map(([id]) => id);
1371
- const index = pinnedRight.indexOf(columnId);
1372
- if (index === -1) return 0;
1373
- let offset = 0;
1374
- for (let i = pinnedRight.length - 1; i > index; i--) {
1375
- const col = columns.find((c) => c.id === pinnedRight[i]);
1376
- offset += col?.minWidth || col?.width || 100;
1377
- }
1378
- return offset;
1379
- },
1380
- [pinnedColumns, columns]
1381
- );
1382
- return {
1383
- pinnedColumns,
1384
- isRowIndexPinned,
1385
- collapsedGroups,
1386
- visibleColumns,
1387
- handleTogglePin,
1388
- handleToggleGroupCollapse,
1389
- setPinnedColumnsFromIds,
1390
- getColumnLeftOffset,
1391
- getColumnRightOffset,
1392
- isColumnPinned,
1393
- getColumnPinSide
1394
- };
1395
- }
1396
-
1397
1404
  // src/components/RowIndexColumnHeader.tsx
1398
1405
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1399
1406
  var cellPaddingCompact3 = "px-1 py-0.5";
@@ -3877,9 +3884,7 @@ function Spreadsheet({
3877
3884
  }
3878
3885
  }
3879
3886
  }
3880
- const allGroupedIds = new Set(
3881
- columnGroups.flatMap((g) => g.columns)
3882
- );
3887
+ const allGroupedIds = new Set(columnGroups.flatMap((g) => g.columns));
3883
3888
  for (const col of visibleColumns) {
3884
3889
  if (!allGroupedIds.has(col.id)) {
3885
3890
  const pinSide = pinnedColumns.get(col.id);
@@ -3991,7 +3996,7 @@ function Spreadsheet({
3991
3996
  style: {
3992
3997
  zoom: zoom / 100
3993
3998
  },
3994
- children: /* @__PURE__ */ jsxs12("table", { className: "w-full border-separate border-spacing-0 text-xs select-none", children: [
3999
+ children: /* @__PURE__ */ jsxs12("table", { className: "border-separate border-spacing-0 text-xs select-none", children: [
3995
4000
  /* @__PURE__ */ jsxs12("thead", { children: [
3996
4001
  columnGroups && groupHeaderItems && /* @__PURE__ */ jsxs12("tr", { children: [
3997
4002
  /* @__PURE__ */ jsx12(
@@ -4008,9 +4013,7 @@ function Spreadsheet({
4008
4013
  ),
4009
4014
  groupHeaderItems.map((item) => {
4010
4015
  if (item.type === "pinned-column") {
4011
- const col = columns.find(
4012
- (c) => c.id === item.columnId
4013
- );
4016
+ const col = columns.find((c) => c.id === item.columnId);
4014
4017
  const isPinnedLeft = item.pinSide === "left";
4015
4018
  return /* @__PURE__ */ jsx12(
4016
4019
  "th",
@@ -4024,9 +4027,9 @@ function Spreadsheet({
4024
4027
  position: "sticky",
4025
4028
  left: isPinnedLeft ? `${getColumnLeftOffset(item.columnId)}px` : void 0,
4026
4029
  right: !isPinnedLeft ? `${getColumnRightOffset(item.columnId)}px` : void 0,
4027
- minWidth: col?.minWidth || col?.width,
4028
- width: col?.minWidth || col?.width,
4029
- maxWidth: 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)
4030
4033
  }
4031
4034
  },
4032
4035
  `pinned-group-${item.columnId}`