@lotics/ui 2.6.1 → 3.1.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.
Files changed (46) hide show
  1. package/package.json +2 -15
  2. package/src/react_native.d.ts +2 -2
  3. package/src/segmented_control.tsx +201 -0
  4. package/src/cell_date.tsx +0 -30
  5. package/src/cell_date_format.test.ts +0 -32
  6. package/src/cell_date_format.ts +0 -73
  7. package/src/cell_number.test.ts +0 -42
  8. package/src/cell_number.tsx +0 -25
  9. package/src/cell_number_format.ts +0 -42
  10. package/src/cell_select.tsx +0 -68
  11. package/src/cell_text.tsx +0 -45
  12. package/src/grid/data_grid.tsx +0 -2003
  13. package/src/grid/data_grid_columns.test.ts +0 -72
  14. package/src/grid/data_grid_columns.ts +0 -30
  15. package/src/grid/data_grid_context.ts +0 -119
  16. package/src/grid/dispatch_safely.ts +0 -39
  17. package/src/grid/engine.module.css +0 -114
  18. package/src/grid/engine.tsx +0 -1042
  19. package/src/grid/helpers.ts +0 -205
  20. package/src/grid/layout.test.ts +0 -515
  21. package/src/grid/layout.ts +0 -425
  22. package/src/grid/recycling.test.ts +0 -236
  23. package/src/grid/recycling.ts +0 -172
  24. package/src/grid/row_cell.module.css +0 -105
  25. package/src/grid/row_cell.tsx +0 -313
  26. package/src/grid/search_highlight.ts +0 -71
  27. package/src/grid/select_cell.tsx +0 -58
  28. package/src/grid/select_group_summary_cell.tsx +0 -76
  29. package/src/grid/select_header_cell.tsx +0 -32
  30. package/src/grid/skeleton_row.module.css +0 -34
  31. package/src/grid/skeleton_row.tsx +0 -20
  32. package/src/grid/use_grid_groups.ts +0 -311
  33. package/src/grid/use_scroll_to_cell.ts +0 -135
  34. package/src/grid/use_virtual_grid.ts +0 -383
  35. package/src/grid/visibility.test.ts +0 -208
  36. package/src/grid/visibility.ts +0 -77
  37. package/src/kanban/constants.ts +0 -18
  38. package/src/kanban/default_renderers.tsx +0 -160
  39. package/src/kanban/drag_preview.tsx +0 -157
  40. package/src/kanban/index.ts +0 -13
  41. package/src/kanban/insert_card_zone.tsx +0 -135
  42. package/src/kanban/kanban_board.tsx +0 -635
  43. package/src/kanban/kanban_card.tsx +0 -321
  44. package/src/kanban/kanban_column.tsx +0 -499
  45. package/src/kanban/placeholders.tsx +0 -54
  46. package/src/kanban/types.ts +0 -116
@@ -1,383 +0,0 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import {
3
- GridGroup,
4
- GridRow,
5
- GroupPath,
6
- GroupPathKey,
7
- groupPathToKey,
8
- RowPath,
9
- useGridLayout,
10
- } from "./layout";
11
- import { RecycledColumn, RecycledRow, useColumnsRecycler, useRowsRecycler } from "./recycling";
12
- import { ContentOffset, useScrollToCellOffset } from "./use_scroll_to_cell";
13
- import { getVisibleIndexRange } from "./visibility";
14
-
15
- export interface ScrollToOffsetParams extends Partial<ContentOffset> {
16
- animated?: boolean;
17
- }
18
-
19
- export interface ScrollToCellParams {
20
- rowPath?: RowPath;
21
- column?: number;
22
- animated?: boolean;
23
- }
24
-
25
- export interface GridRef {
26
- scrollToOffset: (params: ScrollToOffsetParams) => void;
27
- scrollToCell: (params: ScrollToCellParams) => void;
28
- }
29
-
30
- interface ColumnGroup {
31
- width: number;
32
- columns: RecycledColumn[];
33
- }
34
-
35
- interface UseVirtualGridProps {
36
- columns: number[];
37
- collapsedRows?: GroupPathKey[] | null;
38
- frozenColumnCount: number;
39
- footerHeight?: number;
40
- groups: GridGroup[];
41
- groupHeadingHeight?: number;
42
- groupSummaryHeight?: number;
43
- headerHeight?: number;
44
- height: number;
45
- leafRowHeight: number;
46
- onEndReached?: () => void;
47
- /** Fires when the visible row range changes. Used for viewport-driven page loading. */
48
- onVisibleRangeChange?: (rowRange: [number, number]) => void;
49
- rowOverscan?: number;
50
- columnOverscan?: number;
51
- spacerHeight?: number;
52
- width: number;
53
- /** Maximum width for frozen columns. If frozen columns exceed this, they will be scaled down proportionally. */
54
- maxFrozenColumnsWidth?: number;
55
- }
56
-
57
- export interface UseVirtualGridResult {
58
- api: GridRef;
59
- layout: {
60
- contentHeight: number;
61
- contentWidth: number;
62
- frozenColumnsWidth: number;
63
- scrollableColumnsWidth: number;
64
- scrollViewHeight: number;
65
- scrollViewWidth: number;
66
- totalHeight: number;
67
- headerHeight: number;
68
- footerHeight: number;
69
- };
70
- frozenColumns: ColumnGroup;
71
- scrollableColumns: ColumnGroup;
72
- recycledRows: RecycledRow[];
73
- rows: GridRow[];
74
- scrollViewRef: React.RefObject<HTMLDivElement | null>;
75
- handleScroll: (event: React.UIEvent<HTMLDivElement, UIEvent>) => void;
76
- isGroupCollapsed: (groupPathKey: GroupPathKey) => boolean;
77
- /** Whether the grid is scrolled horizontally (scrollX > 0) */
78
- isScrolledX: boolean;
79
- }
80
-
81
- export function useVirtualGrid(props: UseVirtualGridProps): UseVirtualGridResult {
82
- const {
83
- collapsedRows = null,
84
- columns,
85
- frozenColumnCount,
86
- footerHeight = 0,
87
- groups,
88
- groupHeadingHeight = 0,
89
- groupSummaryHeight = 0,
90
- headerHeight = 0,
91
- height,
92
- leafRowHeight,
93
- onEndReached,
94
- onVisibleRangeChange,
95
- rowOverscan = 0,
96
- columnOverscan = 0,
97
- spacerHeight = 0,
98
- width,
99
- maxFrozenColumnsWidth,
100
- } = props;
101
-
102
- const scrollViewRef = useRef<HTMLDivElement | null>(null);
103
- const scrollPositionRef = useRef<ContentOffset>({ x: 0, y: 0 });
104
- const onEndReachedRef = useRef(onEndReached);
105
- onEndReachedRef.current = onEndReached;
106
- const onVisibleRangeChangeRef = useRef(onVisibleRangeChange);
107
- onVisibleRangeChangeRef.current = onVisibleRangeChange;
108
- const endReachedFiredRef = useRef(false);
109
- const visibleRangeRef = useRef<{
110
- rowRange: [number, number];
111
- columnRange: [number, number];
112
- } | null>(null);
113
- const rafIdRef = useRef<number | null>(null);
114
-
115
- const [visibleRange, setVisibleRange] = useState<{
116
- rowRange: [number, number];
117
- columnRange: [number, number];
118
- } | null>(null);
119
-
120
- const isScrolledXRef = useRef(false);
121
- const [isScrolledX, setIsScrolledX] = useState(isScrolledXRef.current);
122
-
123
- const {
124
- contentHeight,
125
- contentWidth,
126
- frozenColumns: transformerfrozenColumns,
127
- frozenColumnsWidth,
128
- scrollableColumns: transformerScrollableColumns,
129
- scrollableColumnsWidth,
130
- rows,
131
- } = useGridLayout({
132
- columns,
133
- frozenColumnCount,
134
- groups,
135
- groupHeadingHeight,
136
- groupSummaryHeight,
137
- leafRowHeight,
138
- spacerHeight,
139
- collapsedRows,
140
- maxFrozenColumnsWidth,
141
- });
142
-
143
- const scrollViewHeight = useMemo(
144
- () => height - headerHeight - footerHeight,
145
- [height, headerHeight, footerHeight],
146
- );
147
- const scrollViewWidth = useMemo(() => width - frozenColumnsWidth, [width, frozenColumnsWidth]);
148
- const totalHeight = useMemo(
149
- () => contentHeight + headerHeight + footerHeight,
150
- [contentHeight, headerHeight, footerHeight],
151
- );
152
-
153
- const calculateVisibleRanges = useCallback(() => {
154
- const scrollY = scrollPositionRef.current.y;
155
- const scrollX = scrollPositionRef.current.x;
156
-
157
- const rowRange = getVisibleIndexRange({
158
- items: rows,
159
- getItemOffset: (row) => row.y,
160
- getItemSize: (row) => row.height,
161
- scrollOffset: scrollY,
162
- scrollViewSize: scrollViewHeight,
163
- overscan: rowOverscan,
164
- });
165
-
166
- const columnRange = getVisibleIndexRange({
167
- items: transformerScrollableColumns,
168
- getItemOffset: (col) => col.x,
169
- getItemSize: (col) => col.width,
170
- scrollOffset: scrollX,
171
- scrollViewSize: scrollViewWidth,
172
- overscan: columnOverscan,
173
- });
174
-
175
- return { rowRange, columnRange };
176
- }, [
177
- rows,
178
- transformerScrollableColumns,
179
- scrollViewHeight,
180
- scrollViewWidth,
181
- rowOverscan,
182
- columnOverscan,
183
- ]);
184
-
185
- const checkAndUpdateVisibleRange = useCallback(() => {
186
- const newRange = calculateVisibleRanges();
187
- const prevRange = visibleRangeRef.current;
188
-
189
- if (
190
- !prevRange ||
191
- prevRange.rowRange[0] !== newRange.rowRange[0] ||
192
- prevRange.rowRange[1] !== newRange.rowRange[1] ||
193
- prevRange.columnRange[0] !== newRange.columnRange[0] ||
194
- prevRange.columnRange[1] !== newRange.columnRange[1]
195
- ) {
196
- visibleRangeRef.current = newRange;
197
- setVisibleRange(newRange);
198
- onVisibleRangeChangeRef.current?.(newRange.rowRange);
199
- }
200
- }, [calculateVisibleRanges]);
201
-
202
- const frozenColumnsEndIndex = useMemo(
203
- () => Math.max(0, transformerfrozenColumns.length - 1),
204
- [transformerfrozenColumns.length],
205
- );
206
-
207
- const recycledRows = useRowsRecycler({
208
- rows,
209
- startIndex: visibleRange?.rowRange[0] ?? 0,
210
- endIndex: visibleRange?.rowRange[1] ?? 0,
211
- });
212
-
213
- const recycledfrozenColumns = useColumnsRecycler({
214
- columns: transformerfrozenColumns,
215
- startIndex: 0,
216
- endIndex: frozenColumnsEndIndex,
217
- });
218
-
219
- const recycledScrollableColumns = useColumnsRecycler({
220
- columns: transformerScrollableColumns,
221
- startIndex: visibleRange?.columnRange[0] ?? 0,
222
- endIndex: visibleRange?.columnRange[1] ?? 0,
223
- });
224
-
225
- const scrollXForScrollToCellRef = useRef({ current: 0 });
226
- const scrollYForScrollToCellRef = useRef({ current: 0 });
227
-
228
- useEffect(() => {
229
- scrollXForScrollToCellRef.current.current = scrollPositionRef.current.x;
230
- scrollYForScrollToCellRef.current.current = scrollPositionRef.current.y;
231
- });
232
-
233
- const getScrollToCellOffset = useScrollToCellOffset({
234
- rows,
235
- frozenColumnCount,
236
- columns: transformerScrollableColumns,
237
- scrollViewHeight,
238
- scrollViewWidth,
239
- scrollX: scrollXForScrollToCellRef.current,
240
- scrollY: scrollYForScrollToCellRef.current,
241
- });
242
-
243
- const handleScrollToOffset = useCallback((params: ScrollToOffsetParams) => {
244
- if (scrollViewRef.current && (params.x !== undefined || params.y !== undefined)) {
245
- scrollViewRef.current.scrollTo({
246
- left: params.x,
247
- top: params.y,
248
- });
249
- }
250
- }, []);
251
-
252
- const handleScroll = useCallback(
253
- (event: React.UIEvent<HTMLDivElement, UIEvent>) => {
254
- const el = event.currentTarget;
255
- const newY = el.scrollTop;
256
- const newX = el.scrollLeft;
257
-
258
- // Skip sub-pixel scrolling
259
- const deltaY = Math.abs(newY - scrollPositionRef.current.y);
260
- const deltaX = Math.abs(newX - scrollPositionRef.current.x);
261
- if (deltaY < 1 && deltaX < 1) {
262
- return;
263
- }
264
-
265
- scrollPositionRef.current = { y: newY, x: newX };
266
-
267
- // Update horizontal scroll state for frozen column shadow (only if changed)
268
- const newIsScrolledX = newX > 0;
269
- if (isScrolledXRef.current !== newIsScrolledX) {
270
- isScrolledXRef.current = newIsScrolledX;
271
- setIsScrolledX(newIsScrolledX);
272
- }
273
-
274
- // Fire onEndReached when scroll nears the bottom (within 200px threshold)
275
- const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
276
- if (distanceFromBottom < 200 && !endReachedFiredRef.current) {
277
- endReachedFiredRef.current = true;
278
- onEndReachedRef.current?.();
279
- } else if (distanceFromBottom >= 200) {
280
- endReachedFiredRef.current = false;
281
- }
282
-
283
- if (rafIdRef.current === null) {
284
- rafIdRef.current = requestAnimationFrame(() => {
285
- rafIdRef.current = null;
286
- checkAndUpdateVisibleRange();
287
- });
288
- }
289
- },
290
- [checkAndUpdateVisibleRange],
291
- );
292
-
293
- useEffect(() => {
294
- checkAndUpdateVisibleRange();
295
- }, [
296
- checkAndUpdateVisibleRange,
297
- rows,
298
- transformerScrollableColumns,
299
- scrollViewHeight,
300
- scrollViewWidth,
301
- ]);
302
-
303
- useEffect(() => {
304
- return () => {
305
- if (rafIdRef.current !== null) {
306
- cancelAnimationFrame(rafIdRef.current);
307
- }
308
- };
309
- }, []);
310
-
311
- const api = useMemo<GridRef>(
312
- () => ({
313
- scrollToOffset: handleScrollToOffset,
314
- scrollToCell: (params: ScrollToCellParams) =>
315
- handleScrollToOffset(getScrollToCellOffset(params)),
316
- }),
317
- [getScrollToCellOffset, handleScrollToOffset],
318
- );
319
-
320
- const frozenColumns: ColumnGroup = useMemo(
321
- () => ({
322
- width: frozenColumnsWidth,
323
- columns: recycledfrozenColumns,
324
- }),
325
- [frozenColumnsWidth, recycledfrozenColumns],
326
- );
327
-
328
- const scrollableColumns: ColumnGroup = useMemo(
329
- () => ({
330
- width: scrollableColumnsWidth,
331
- columns: recycledScrollableColumns,
332
- }),
333
- [scrollableColumnsWidth, recycledScrollableColumns],
334
- );
335
-
336
- const collapsedGroupKeys = useMemo(() => {
337
- if (!collapsedRows || collapsedRows.length === 0) {
338
- return null;
339
- }
340
- return new Set(collapsedRows);
341
- }, [collapsedRows]);
342
-
343
- const isGroupCollapsed = useCallback(
344
- (groupPathKey: GroupPathKey) => collapsedGroupKeys?.has(groupPathKey) ?? false,
345
- [collapsedGroupKeys],
346
- );
347
-
348
- return {
349
- api,
350
- frozenColumns,
351
- scrollableColumns,
352
- recycledRows,
353
- rows,
354
- scrollViewRef,
355
- handleScroll,
356
- isGroupCollapsed,
357
- isScrolledX,
358
- layout: useMemo(
359
- () => ({
360
- contentHeight,
361
- contentWidth,
362
- frozenColumnsWidth,
363
- scrollableColumnsWidth,
364
- scrollViewHeight,
365
- scrollViewWidth,
366
- totalHeight,
367
- headerHeight,
368
- footerHeight,
369
- }),
370
- [
371
- contentHeight,
372
- contentWidth,
373
- frozenColumnsWidth,
374
- scrollableColumnsWidth,
375
- scrollViewHeight,
376
- scrollViewWidth,
377
- totalHeight,
378
- headerHeight,
379
- footerHeight,
380
- ],
381
- ),
382
- };
383
- }
@@ -1,208 +0,0 @@
1
- import { getVisibleIndexRange } from "./visibility";
2
-
3
- test("getVisibleIndexRange - basic visibility", () => {
4
- const items = [
5
- { size: 100, offset: 0 },
6
- { size: 200, offset: 100 },
7
- { size: 100, offset: 300 },
8
- { size: 200, offset: 400 },
9
- { size: 100, offset: 600 },
10
- { size: 200, offset: 700 },
11
- ];
12
-
13
- const scrollViewSize = 400;
14
-
15
- const table = [
16
- [0, 0, 2],
17
- [100, 1, 3],
18
- [350, 2, 5],
19
- [500, 3, 5],
20
- ];
21
-
22
- for (let i = 0; i < table.length; i++) {
23
- const [scrollOffset, startIndex, endIndex] = table[i];
24
-
25
- const result = getVisibleIndexRange({
26
- items,
27
- scrollOffset,
28
- scrollViewSize,
29
- getItemOffset: (column) => column.offset,
30
- getItemSize: (column) => column.size,
31
- });
32
-
33
- expect(result).toEqual([startIndex, endIndex]);
34
- }
35
- });
36
-
37
- test("getVisibleIndexRange - empty items", () => {
38
- const result = getVisibleIndexRange<{ offset: number; size: number }>({
39
- items: [],
40
- scrollOffset: 0,
41
- scrollViewSize: 400,
42
- getItemOffset: (item) => item.offset,
43
- getItemSize: (item) => item.size,
44
- });
45
-
46
- expect(result).toEqual([0, 0]);
47
- });
48
-
49
- test("getVisibleIndexRange - single item visible", () => {
50
- const items = [
51
- { size: 100, offset: 0 },
52
- { size: 100, offset: 100 },
53
- { size: 100, offset: 200 },
54
- ];
55
-
56
- const result = getVisibleIndexRange({
57
- items,
58
- scrollOffset: 100,
59
- scrollViewSize: 100,
60
- getItemOffset: (item) => item.offset,
61
- getItemSize: (item) => item.size,
62
- });
63
-
64
- expect(result).toEqual([1, 1]);
65
- });
66
-
67
- test("getVisibleIndexRange - with overscan in middle", () => {
68
- const items = [
69
- { size: 100, offset: 0 },
70
- { size: 100, offset: 100 },
71
- { size: 100, offset: 200 },
72
- { size: 100, offset: 300 },
73
- { size: 100, offset: 400 },
74
- ];
75
-
76
- // In the middle: visible item is index 2 (offset 200-300)
77
- // With overscan=1: should overscan backward to [1] and forward to [3]
78
- const result = getVisibleIndexRange({
79
- items,
80
- scrollOffset: 200,
81
- scrollViewSize: 100,
82
- getItemOffset: (item) => item.offset,
83
- getItemSize: (item) => item.size,
84
- overscan: 1,
85
- });
86
-
87
- // Without overscan: [2, 2] (only item 2 visible)
88
- // With overscan=1: [1, 3] (overscans 1 backward and 1 forward)
89
- expect(result).toEqual([1, 3]);
90
- });
91
-
92
- test("getVisibleIndexRange - overscan in middle with larger overscan", () => {
93
- const items = [
94
- { size: 100, offset: 0 },
95
- { size: 100, offset: 100 },
96
- { size: 100, offset: 200 },
97
- { size: 100, offset: 300 },
98
- { size: 100, offset: 400 },
99
- { size: 100, offset: 500 },
100
- ];
101
-
102
- // In the middle: visible item is index 2 (offset 200-300)
103
- // With overscan=2: should overscan backward to [0, 1] and forward to [3, 4]
104
- const result = getVisibleIndexRange({
105
- items,
106
- scrollOffset: 200,
107
- scrollViewSize: 100,
108
- getItemOffset: (item) => item.offset,
109
- getItemSize: (item) => item.size,
110
- overscan: 2,
111
- });
112
-
113
- // Visible: [2, 2]
114
- // With overscan=2: [max(2-2, 0), min(2+2, 5)] = [0, 4]
115
- expect(result).toEqual([0, 4]);
116
- });
117
-
118
- test("getVisibleIndexRange - overscan at boundaries", () => {
119
- const items = [
120
- { size: 100, offset: 0 },
121
- { size: 100, offset: 100 },
122
- { size: 100, offset: 200 },
123
- ];
124
-
125
- // At start - overscan forward is possible (items exist after visible range)
126
- const resultStart = getVisibleIndexRange({
127
- items,
128
- scrollOffset: 0,
129
- scrollViewSize: 100,
130
- getItemOffset: (item) => item.offset,
131
- getItemSize: (item) => item.size,
132
- overscan: 5,
133
- });
134
-
135
- // Can't overscan backward (already at 0), but can overscan forward
136
- // Visible: [0], with overscan: [0, min(0+5, 2)] = [0, 2]
137
- expect(resultStart).toEqual([0, 2]);
138
-
139
- // At end - overscan backward is possible (items exist before visible range)
140
- const resultEnd = getVisibleIndexRange({
141
- items,
142
- scrollOffset: 200,
143
- scrollViewSize: 100,
144
- getItemOffset: (item) => item.offset,
145
- getItemSize: (item) => item.size,
146
- overscan: 5,
147
- });
148
-
149
- // Can't overscan forward (already at end), but can overscan backward
150
- // Visible: [2], with overscan: [max(2-5, 0), 2] = [0, 2]
151
- expect(resultEnd).toEqual([0, 2]);
152
- });
153
-
154
- test("getVisibleIndexRange - item spans viewport boundary", () => {
155
- const items = [
156
- { size: 100, offset: 0 },
157
- { size: 300, offset: 100 }, // Large item that spans multiple positions
158
- { size: 100, offset: 400 },
159
- ];
160
-
161
- const result = getVisibleIndexRange({
162
- items,
163
- scrollOffset: 50,
164
- scrollViewSize: 200,
165
- getItemOffset: (item) => item.offset,
166
- getItemSize: (item) => item.size,
167
- });
168
-
169
- // Should include the large item that crosses the boundary
170
- expect(result).toEqual([0, 1]);
171
- });
172
-
173
- test("getVisibleIndexRange - all items visible", () => {
174
- const items = [
175
- { size: 50, offset: 0 },
176
- { size: 50, offset: 50 },
177
- { size: 50, offset: 100 },
178
- ];
179
-
180
- const result = getVisibleIndexRange({
181
- items,
182
- scrollOffset: 0,
183
- scrollViewSize: 200, // Larger than all items
184
- getItemOffset: (item) => item.offset,
185
- getItemSize: (item) => item.size,
186
- });
187
-
188
- expect(result).toEqual([0, 2]);
189
- });
190
-
191
- test("getVisibleIndexRange - scrolled past all items", () => {
192
- const items = [
193
- { size: 100, offset: 0 },
194
- { size: 100, offset: 100 },
195
- { size: 100, offset: 200 },
196
- ];
197
-
198
- const result = getVisibleIndexRange({
199
- items,
200
- scrollOffset: 500, // Way past all items
201
- scrollViewSize: 100,
202
- getItemOffset: (item) => item.offset,
203
- getItemSize: (item) => item.size,
204
- });
205
-
206
- // Should return last item
207
- expect(result).toEqual([2, 2]);
208
- });
@@ -1,77 +0,0 @@
1
- import { max, min } from "./helpers";
2
-
3
- interface GetVisibleIndexRangeParams<T> {
4
- items: T[];
5
- scrollOffset: number;
6
- scrollViewSize: number;
7
- getItemOffset: (item: T) => number;
8
- getItemSize: (item: T) => number;
9
- overscan?: number;
10
- }
11
-
12
- type VisibleIndexRange = [startIndex: number, endIndex: number];
13
-
14
- export function getVisibleIndexRange<T>(params: GetVisibleIndexRangeParams<T>): VisibleIndexRange {
15
- const { items, scrollOffset, scrollViewSize, getItemOffset, getItemSize, overscan = 0 } = params;
16
-
17
- if (items.length === 0) {
18
- return [0, 0];
19
- }
20
-
21
- // Binary search for start index
22
- let left = 0;
23
- let right = items.length - 1;
24
- let startIndex = 0;
25
-
26
- while (left <= right) {
27
- const mid = Math.floor((left + right) / 2);
28
- const offset = getItemOffset(items[mid]);
29
-
30
- if (offset <= scrollOffset) {
31
- startIndex = mid;
32
- left = mid + 1;
33
- } else {
34
- right = mid - 1;
35
- }
36
- }
37
-
38
- // Binary search for end index
39
- const scrollEnd = scrollOffset + scrollViewSize;
40
- left = startIndex;
41
- right = items.length - 1;
42
- let endIndex = startIndex;
43
-
44
- while (left <= right) {
45
- const mid = Math.floor((left + right) / 2);
46
- const item = items[mid];
47
- const itemEnd = getItemOffset(item) + getItemSize(item);
48
-
49
- if (itemEnd < scrollEnd) {
50
- endIndex = mid;
51
- left = mid + 1;
52
- } else {
53
- right = mid - 1;
54
- }
55
- }
56
-
57
- // Ensure we include the item that crosses the boundary
58
- if (endIndex < items.length - 1) {
59
- const nextItem = items[endIndex + 1];
60
- if (getItemOffset(nextItem) < scrollEnd) {
61
- endIndex++;
62
- }
63
- }
64
-
65
- // Apply overscan in both directions when possible
66
- if (overscan > 0) {
67
- if (startIndex > 0) {
68
- startIndex = max(startIndex - overscan, 0);
69
- }
70
-
71
- if (endIndex < items.length - 1) {
72
- endIndex = min(endIndex + overscan, items.length - 1);
73
- }
74
- }
75
-
76
- return [startIndex, endIndex];
77
- }
@@ -1,18 +0,0 @@
1
- // Column layout
2
- export const DEFAULT_COLUMN_WIDTH = 280;
3
- export const MINIMIZED_COLUMN_WIDTH = 44;
4
- export const DEFAULT_COLUMN_GAP = 16;
5
- export const DEFAULT_ITEM_GAP = 8;
6
- export const COLUMN_CONTENT_PADDING = 12;
7
-
8
- // Auto-scroll behavior
9
- export const AUTO_SCROLL_THRESHOLD = 60;
10
- export const AUTO_SCROLL_SPEED = 8;
11
-
12
- // Drag detection
13
- export const DRAG_THRESHOLD = 5;
14
-
15
- // Touch-specific drag activation (like dnd-kit's delay activation)
16
- // On touch devices, require a hold before drag can start to distinguish from tap
17
- export const TOUCH_DRAG_DELAY = 200; // ms - hold time before drag activates on touch
18
- export const TOUCH_TOLERANCE = 10; // px - max movement allowed during delay period