@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,172 +0,0 @@
1
- import React, { useMemo, useRef } from "react";
2
- import { differenceBy, intersectBy, isEmpty, max, maxBy, min } from "./helpers";
3
- import {
4
- GridColumn,
5
- GridDataRow,
6
- GridGroupHeadingRow,
7
- GridGroupSummaryRow,
8
- GridRow,
9
- GridSpacerRow,
10
- } from "./layout";
11
-
12
- export interface RecycledDataRow extends GridDataRow {
13
- key: number;
14
- }
15
-
16
- export interface RecycledGroupHeadingRow extends GridGroupHeadingRow {
17
- key: number;
18
- }
19
-
20
- export interface RecycledGroupSummaryRow extends GridGroupSummaryRow {
21
- key: number;
22
- }
23
-
24
- export interface RecycledSpacerRow extends GridSpacerRow {
25
- key: number;
26
- }
27
-
28
- export type RecycledRow =
29
- | RecycledDataRow
30
- | RecycledGroupHeadingRow
31
- | RecycledGroupSummaryRow
32
- | RecycledSpacerRow;
33
-
34
- export interface RecycledColumn extends GridColumn {
35
- key: number;
36
- }
37
-
38
- export interface UseRowsRecyclerProps {
39
- rows: GridRow[];
40
- startIndex: number;
41
- endIndex: number;
42
- }
43
-
44
- export interface UseColumnsRecyclerProps {
45
- columns: GridColumn[];
46
- startIndex: number;
47
- endIndex: number;
48
- }
49
-
50
- interface RecycleItemsParams<T, K extends T> {
51
- items: T[];
52
- prevItems: K[];
53
- toRecycledItem: (item: T, key: number) => K;
54
- /** Value to recycle by */
55
- getValue: (item: T | K) => number;
56
- getKey: (item: K) => number;
57
- }
58
-
59
- interface RecycleRowsParams {
60
- rows: GridRow[];
61
- prevRows: RecycledRow[];
62
- startIndex: number;
63
- endIndex: number;
64
- }
65
-
66
- interface RecycleColumnsParams {
67
- columns: GridColumn[];
68
- prevColumns: RecycledColumn[];
69
- startIndex: number;
70
- endIndex: number;
71
- }
72
-
73
- export function recycleItems<T, K extends T>(params: RecycleItemsParams<T, K>): K[] {
74
- const { items, prevItems, getValue, toRecycledItem, getKey } = params;
75
-
76
- const reusedItems = intersectBy(prevItems, items, getValue, true);
77
- const recycledItems = differenceBy(prevItems, items, getValue);
78
- const newItems = differenceBy(items, prevItems, getValue);
79
-
80
- if (isEmpty(prevItems)) {
81
- return items.map((item, key) => toRecycledItem(item, key));
82
- }
83
-
84
- const recycledKeys = recycledItems.map((c) => getKey(c));
85
- if (recycledKeys.length < newItems.length) {
86
- let maxKey = maxBy(prevItems, getKey) as number;
87
-
88
- while (recycledKeys.length !== newItems.length) {
89
- recycledKeys.push(++maxKey);
90
- }
91
- }
92
-
93
- return reusedItems
94
- .concat(newItems.map((item, i) => toRecycledItem(item, recycledKeys[i])))
95
- .sort((a, b) => getValue(a) - getValue(b));
96
- }
97
-
98
- export function useRowsRecycler(props: UseRowsRecyclerProps): RecycledRow[] {
99
- const { rows, startIndex, endIndex } = props;
100
-
101
- const prevRecycledRowsRef = useRef<RecycledRow[]>([]);
102
-
103
- const recycledRows = useMemo(
104
- (): RecycledRow[] =>
105
- recycleRows({
106
- rows,
107
- prevRows: prevRecycledRowsRef.current,
108
- startIndex,
109
- endIndex,
110
- }),
111
- [rows, startIndex, endIndex],
112
- );
113
-
114
- prevRecycledRowsRef.current = recycledRows;
115
-
116
- return recycledRows;
117
- }
118
-
119
- export function useColumnsRecycler(props: UseColumnsRecyclerProps): RecycledColumn[] {
120
- const { columns, startIndex, endIndex } = props;
121
-
122
- const prevRecycledColumnsRef = useRef<RecycledColumn[]>([]);
123
-
124
- const recycledColumns = useMemo(
125
- (): RecycledColumn[] =>
126
- recycleColumns({
127
- columns,
128
- prevColumns: prevRecycledColumnsRef.current,
129
- startIndex,
130
- endIndex,
131
- }),
132
- [columns, startIndex, endIndex],
133
- );
134
-
135
- prevRecycledColumnsRef.current = recycledColumns;
136
-
137
- return recycledColumns;
138
- }
139
-
140
- function recycleRows(params: RecycleRowsParams) {
141
- const { rows, prevRows, startIndex, endIndex } = params;
142
-
143
- const nextRows = rows.slice(startIndex, endIndex + 1);
144
-
145
- return recycleItems({
146
- items: nextRows,
147
- prevItems: prevRows,
148
- getValue: (row) => row.y,
149
- getKey: (row) => row.key,
150
- toRecycledItem: (row, key) => ({
151
- ...row,
152
- key,
153
- }),
154
- });
155
- }
156
-
157
- function recycleColumns(params: RecycleColumnsParams) {
158
- const { columns, prevColumns, startIndex, endIndex } = params;
159
-
160
- const nextColumns = columns.slice(startIndex, endIndex + 1);
161
-
162
- return recycleItems({
163
- items: nextColumns,
164
- prevItems: prevColumns,
165
- getValue: (column) => column.column,
166
- getKey: (column) => column.key,
167
- toRecycledItem: (column, key) => ({
168
- ...column,
169
- key,
170
- }),
171
- });
172
- }
@@ -1,105 +0,0 @@
1
- .row_cell {
2
- width: 100%;
3
- height: 100%;
4
- position: relative;
5
- outline: none;
6
- border-bottom: 1px solid var(--color-border);
7
- box-sizing: border-box;
8
- overflow: hidden;
9
- }
10
-
11
- /* Active cell (most recently selected) */
12
- .row_cell_active {
13
- outline: 2px solid var(--color-blue-500);
14
- outline-offset: -2px;
15
- z-index: 2;
16
- }
17
-
18
- .row_cell_frozen {
19
- background-color: var(--color-background);
20
- }
21
-
22
- .row_cell_in_selected_row {
23
- background-color: var(--color-blue-50);
24
- }
25
-
26
- /* Selected cell styling */
27
- .row_cell_selected {
28
- background-color: var(--color-blue-50);
29
- }
30
-
31
- /* Selected but not active */
32
- .row_cell_selected:not(.row_cell_active) {
33
- outline: 1px solid var(--color-zinc-300);
34
- }
35
-
36
- .row_cell_fill_handle {
37
- position: absolute;
38
- bottom: -4px;
39
- right: -4px;
40
- width: 10px;
41
- height: 10px;
42
- background-color: var(--color-blue-500);
43
- border: 1px solid var(--color-white);
44
- cursor: crosshair;
45
- z-index: 10;
46
- }
47
-
48
- .row_cell_fill_handle:hover {
49
- background-color: var(--color-blue-600);
50
- transform: scale(1.2);
51
- }
52
-
53
- /* Fill preview styling */
54
- .row_cell_fill_preview {
55
- background-color: var(--color-blue-100);
56
- outline: 1px solid var(--color-blue-400);
57
- }
58
-
59
- /* Value change animation */
60
- .row_cell_value_changed {
61
- animation: valueChangeFlash 0.6s ease-out;
62
- }
63
-
64
- @keyframes valueChangeFlash {
65
- 0% {
66
- box-shadow: inset 0 0 0 1000px var(--color-blue-200);
67
- }
68
- 100% {
69
- box-shadow: inset 0 0 0 1000px transparent;
70
- }
71
- }
72
-
73
- /* Search highlight animation alternates class names so repeated reveals on
74
- the same row restart the animation. */
75
- .row_cell_search_highlight_even {
76
- animation: searchHighlightEven 1.5s ease-out;
77
- }
78
-
79
- .row_cell_search_highlight_odd {
80
- animation: searchHighlightOdd 1.5s ease-out;
81
- }
82
-
83
- @keyframes searchHighlightEven {
84
- 0% {
85
- box-shadow: inset 0 0 0 1000px var(--color-amber-100);
86
- }
87
- 70% {
88
- box-shadow: inset 0 0 0 1000px var(--color-amber-100);
89
- }
90
- 100% {
91
- box-shadow: inset 0 0 0 1000px transparent;
92
- }
93
- }
94
-
95
- @keyframes searchHighlightOdd {
96
- 0% {
97
- box-shadow: inset 0 0 0 1000px var(--color-amber-100);
98
- }
99
- 70% {
100
- box-shadow: inset 0 0 0 1000px var(--color-amber-100);
101
- }
102
- 100% {
103
- box-shadow: inset 0 0 0 1000px transparent;
104
- }
105
- }
@@ -1,313 +0,0 @@
1
- import { memo, useCallback, useEffect, useRef, useState } from "react";
2
- import { DataGridColumn, RowPathKey } from "./data_grid";
3
- import { RowId, useCellAnimation } from "./data_grid_context";
4
- import { useSearchHighlight } from "./search_highlight";
5
-
6
- import styles from "./row_cell.module.css";
7
-
8
- export type NavigationDirection = "up" | "down" | "left" | "right";
9
-
10
- interface RowCellProps<TRow, TValue> {
11
- rowKey: RowPathKey;
12
- rowId: RowId;
13
- row: TRow;
14
- value: TValue;
15
- column: DataGridColumn<TRow>;
16
- columnIdx: number;
17
- active: boolean;
18
- selected: boolean;
19
- frozen: boolean;
20
- inSelectedRow: boolean;
21
- editing: boolean;
22
- /** Row index for fill operations */
23
- rowIndex: number;
24
- /** Whether this cell is in the fill range (target cells during drag) */
25
- inFillRange: boolean;
26
- /** Background color for the row (from conditional coloring) */
27
- rowBackgroundColor?: string;
28
- onStartEditing: (rowKey: RowPathKey, columnIdx: number) => void;
29
- onSelectStart: (rowKey: RowPathKey, columnIdx: number) => void;
30
- onSelectUpdate: (rowKey: RowPathKey, columnIdx: number) => void;
31
- onCommit: (nextValue: unknown, columnKey: string, rowKey: RowPathKey) => void;
32
- onCancelEditing: () => void;
33
- onNavigate: (direction: NavigationDirection, extend: boolean) => void;
34
- onFillStart: (sourceRowIndex: number, column: number) => void;
35
- onRequestLockedFieldChange?: (row: TRow, columnKey: string) => void;
36
- }
37
-
38
- export const RowCell = memo(function RowCell<TRow, TValue>(props: RowCellProps<TRow, TValue>) {
39
- const {
40
- rowKey,
41
- rowId,
42
- row,
43
- value,
44
- column,
45
- columnIdx,
46
- active,
47
- selected,
48
- frozen,
49
- inSelectedRow,
50
- editing,
51
- rowIndex,
52
- inFillRange,
53
- rowBackgroundColor,
54
- onStartEditing,
55
- onSelectStart,
56
- onSelectUpdate,
57
- onCommit,
58
- onCancelEditing,
59
- onNavigate,
60
- onFillStart,
61
- onRequestLockedFieldChange,
62
- } = props;
63
-
64
- // Cell animation hook resolved via context. Records page provides one
65
- // bound to useRealtimeChangeAnimation(tableId, …); iframe apps fall through
66
- // to the noop default.
67
- const isAnimating = useCellAnimation(rowId, column.key);
68
- const searchHighlightRevealId = useSearchHighlight(rowId);
69
-
70
- const editable = column.isCellEditable?.(row, column.key) ?? true;
71
-
72
- const handleMouseDown = useCallback(
73
- (event: React.MouseEvent) => {
74
- // Only handle left mouse button
75
- if (event.button !== 0) return;
76
- if (event.shiftKey) {
77
- // Shift+click extends selection
78
- onSelectUpdate(rowKey, columnIdx);
79
- } else {
80
- // Start new selection
81
- onSelectStart(rowKey, columnIdx);
82
- }
83
- },
84
- [onSelectStart, onSelectUpdate, rowKey, columnIdx],
85
- );
86
-
87
- const handleMouseEnter = useCallback(
88
- (event: React.MouseEvent) => {
89
- // Extend selection if mouse button is pressed (dragging)
90
- if (event.buttons === 1) {
91
- onSelectUpdate(rowKey, columnIdx);
92
- }
93
- },
94
- [onSelectUpdate, rowKey, columnIdx],
95
- );
96
-
97
- // Fill handle: start drag (just triggers callback, global listeners managed by DataGrid)
98
- const handleFillHandleMouseDown = useCallback(
99
- (event: React.MouseEvent) => {
100
- event.preventDefault();
101
- event.stopPropagation();
102
- onFillStart(rowIndex, columnIdx);
103
- },
104
- [rowIndex, columnIdx, onFillStart],
105
- );
106
-
107
- const handleDoubleClick = useCallback(() => {
108
- if (editable && column.renderEditCell) {
109
- onStartEditing(rowKey, columnIdx);
110
- } else if (!editable && onRequestLockedFieldChange) {
111
- onRequestLockedFieldChange(row, column.key);
112
- }
113
- }, [
114
- editable,
115
- column.renderEditCell,
116
- column.key,
117
- onStartEditing,
118
- onRequestLockedFieldChange,
119
- row,
120
- rowKey,
121
- columnIdx,
122
- rowId,
123
- ]);
124
-
125
- // Stable callback for cell value changes (avoids inline closure in renderCell)
126
- const handleValueChange = useCallback(
127
- (nextValue: TValue) => {
128
- onCommit(nextValue, column.key, rowKey);
129
- },
130
- [onCommit, column.key, rowKey],
131
- );
132
-
133
- // Compute CSS class based on active and selected states
134
- let className = styles.row_cell;
135
- if (active) {
136
- className += ` ${styles.row_cell_active}`;
137
- }
138
- if (selected) {
139
- className += ` ${styles.row_cell_selected}`;
140
- }
141
- if (frozen) {
142
- className += ` ${styles.row_cell_frozen}`;
143
- }
144
- if (inSelectedRow) {
145
- className += ` ${styles.row_cell_in_selected_row}`;
146
- }
147
- if (inFillRange) {
148
- className += ` ${styles.row_cell_fill_preview}`;
149
- }
150
- if (isAnimating) {
151
- className += ` ${styles.row_cell_value_changed}`;
152
- }
153
- if (searchHighlightRevealId !== null) {
154
- className +=
155
- searchHighlightRevealId % 2 === 0
156
- ? ` ${styles.row_cell_search_highlight_even}`
157
- : ` ${styles.row_cell_search_highlight_odd}`;
158
- }
159
-
160
- // Show fill handle on active cell when editable and has renderEditCell
161
- const showFillHandle = active && !editing && editable;
162
-
163
- // Render editing cell as a separate component so it mounts fresh,
164
- // initializing draft state correctly without flash
165
- if (editing && editable && column.renderEditCell) {
166
- return (
167
- <RowCellEditor
168
- className={className}
169
- rowKey={rowKey}
170
- rowId={rowId}
171
- row={row}
172
- value={value}
173
- column={column}
174
- onCommit={onCommit}
175
- onCancelEditing={onCancelEditing}
176
- onNavigate={onNavigate}
177
- />
178
- );
179
- }
180
-
181
- // Apply row background color when not selected
182
- const cellStyle =
183
- rowBackgroundColor && !selected && !inSelectedRow
184
- ? { backgroundColor: rowBackgroundColor }
185
- : undefined;
186
-
187
- return (
188
- <div
189
- data-row-index={rowIndex}
190
- data-row-key={rowKey}
191
- data-row-id={rowId}
192
- data-column={columnIdx}
193
- onMouseDown={handleMouseDown}
194
- onMouseEnter={handleMouseEnter}
195
- onDoubleClick={handleDoubleClick}
196
- className={className}
197
- style={cellStyle}
198
- >
199
- {column.renderCell?.({
200
- rowKey,
201
- rowId,
202
- row,
203
- columnKey: column.key,
204
- value,
205
- active,
206
- editable,
207
- onValueChange: handleValueChange,
208
- })}
209
- {showFillHandle && (
210
- <div className={styles.row_cell_fill_handle} onMouseDown={handleFillHandleMouseDown} />
211
- )}
212
- </div>
213
- );
214
- }) as <TRow, TValue>(props: RowCellProps<TRow, TValue>) => React.ReactNode;
215
-
216
- /**
217
- * Separate component for editing mode.
218
- * Mounts fresh each time editing starts, so useState initializes correctly.
219
- */
220
- interface RowCellEditorProps<TRow, TValue> {
221
- className: string;
222
- rowKey: RowPathKey;
223
- rowId: RowId;
224
- row: TRow;
225
- value: TValue;
226
- column: DataGridColumn<TRow>;
227
- onCommit: (nextValue: unknown, columnKey: string, rowKey: RowPathKey) => void;
228
- onCancelEditing: () => void;
229
- onNavigate: (direction: NavigationDirection, extend: boolean) => void;
230
- }
231
-
232
- function RowCellEditor<TRow, TValue>(props: RowCellEditorProps<TRow, TValue>) {
233
- const { className, rowKey, row, rowId, value, column, onCommit, onCancelEditing, onNavigate } =
234
- props;
235
-
236
- // State initializes fresh each time this component mounts (editing starts)
237
- const [draftValue, setDraftValue] = useState<TValue>(value);
238
- const draftValueRef = useRef<TValue>(value);
239
- const cancelledRef = useRef(false);
240
-
241
- // Refs for stable access in unmount cleanup (no stale closures)
242
- const onCommitRef = useRef(onCommit);
243
- onCommitRef.current = onCommit;
244
- const rowKeyRef = useRef(rowKey);
245
- rowKeyRef.current = rowKey;
246
- const columnKeyRef = useRef(column.key);
247
- columnKeyRef.current = column.key;
248
-
249
- // Single commit path: flush draft value on unmount unless cancelled.
250
- // All exit paths (Enter, Tab, blur, click-outside) unmount this component;
251
- // the cleanup commits the final draft value exactly once.
252
- // Uses rowKeyRef to identify the correct row regardless of selection changes.
253
- useEffect(() => {
254
- return () => {
255
- if (!cancelledRef.current) {
256
- onCommitRef.current(draftValueRef.current, columnKeyRef.current, rowKeyRef.current);
257
- }
258
- };
259
- // eslint-disable-next-line react-hooks/exhaustive-deps
260
- }, []);
261
-
262
- const handleValueChange = useCallback((nextValue: TValue) => {
263
- setDraftValue(nextValue);
264
- draftValueRef.current = nextValue;
265
- }, []);
266
-
267
- const handleCancel = useCallback(() => {
268
- cancelledRef.current = true;
269
- onCancelEditing();
270
- }, [onCancelEditing]);
271
-
272
- const handleKeyDown = useCallback(
273
- (event: React.KeyboardEvent) => {
274
- if (event.key === "Escape") {
275
- event.preventDefault();
276
- event.stopPropagation();
277
- handleCancel();
278
- return;
279
- }
280
-
281
- if (event.key === "Enter" && !event.shiftKey) {
282
- event.preventDefault();
283
- event.stopPropagation();
284
- onCancelEditing();
285
- return;
286
- }
287
-
288
- if (event.key === "Tab") {
289
- event.preventDefault();
290
- event.stopPropagation();
291
- onCancelEditing();
292
- onNavigate(event.shiftKey ? "left" : "right", false);
293
- return;
294
- }
295
- },
296
- [handleCancel, onCancelEditing, onNavigate],
297
- );
298
-
299
- return (
300
- <div className={className} onKeyDown={handleKeyDown}>
301
- {column.renderEditCell!({
302
- rowKey,
303
- rowId,
304
- row,
305
- columnKey: column.key,
306
- value: draftValue,
307
- onValueChange: handleValueChange,
308
- onClose: onCancelEditing,
309
- onCancel: handleCancel,
310
- })}
311
- </div>
312
- );
313
- }
@@ -1,71 +0,0 @@
1
- import { useSyncExternalStore } from "react";
2
- import { dispatchSafely } from "./dispatch_safely";
3
-
4
- /**
5
- * Lightweight external store for search-highlight state.
6
- * Only the row cells matching the highlighted row re-render
7
- * (useSyncExternalStore compares the boolean selector with Object.is).
8
- *
9
- * Supports ownership: callers pass an `owner` token when setting/clearing.
10
- * A clear with a mismatched owner is a no-op, preventing stale unmount
11
- * cleanup from wiping a newer highlight.
12
- */
13
-
14
- let highlightedId: string | null = null;
15
- let highlightedRevealId: number | null = null;
16
- let currentOwner: string | null = null;
17
- const listeners = new Set<() => void>();
18
-
19
- function notify() {
20
- dispatchSafely(listeners, (listener) => listener());
21
- }
22
-
23
- interface SearchHighlightUpdate {
24
- owner?: string;
25
- revealId?: number;
26
- }
27
-
28
- export function setSearchHighlight(id: string | null, update?: SearchHighlightUpdate) {
29
- if (id === null) {
30
- // Clear only if the caller owns the current highlight (or no owner specified)
31
- if (update?.owner && currentOwner !== update.owner) return;
32
- highlightedId = null;
33
- highlightedRevealId = null;
34
- currentOwner = null;
35
- } else {
36
- highlightedId = id;
37
- highlightedRevealId = update?.revealId ?? null;
38
- currentOwner = update?.owner ?? null;
39
- }
40
- notify();
41
- }
42
-
43
- function subscribe(listener: () => void) {
44
- listeners.add(listener);
45
- return () => {
46
- listeners.delete(listener);
47
- };
48
- }
49
-
50
- /**
51
- * Returns the current reveal token when this row is highlighted, or null otherwise.
52
- * This lets the same row re-render on repeated reveals so the animation can restart.
53
- */
54
- export function useSearchHighlight(rowId: string): number | null {
55
- return useSyncExternalStore(
56
- subscribe,
57
- () => (highlightedId === rowId ? highlightedRevealId : null),
58
- );
59
- }
60
-
61
- /** Exposed for tests only. */
62
- export function _getHighlightState() {
63
- return { highlightedId, highlightedRevealId, currentOwner };
64
- }
65
-
66
- /** Exposed for tests only — resets to clean state between tests. */
67
- export function _resetHighlightState() {
68
- highlightedId = null;
69
- highlightedRevealId = null;
70
- currentOwner = null;
71
- }
@@ -1,58 +0,0 @@
1
- import { Checkbox } from "@lotics/ui/checkbox";
2
- import { Text } from "@lotics/ui/text";
3
- import { useRowSelection } from "./data_grid_context";
4
- import type { RowPathKey } from "@lotics/ui/grid/layout";
5
-
6
- import { memo, useCallback, useMemo } from "react";
7
- import { Pressable, View } from "react-native";
8
-
9
- /** Props for checkbox cell - needs rowKey for row selection */
10
- export interface SelectCellProps {
11
- rowKey: RowPathKey;
12
- }
13
-
14
- export const SelectCell = memo(function SelectCell(props: SelectCellProps) {
15
- const { rowKey } = props;
16
- const { isRowSelected, onRowSelectionChange } = useRowSelection();
17
-
18
- const checked = useMemo(() => isRowSelected(rowKey), [isRowSelected, rowKey]);
19
-
20
- const handlePress = useCallback(() => {
21
- onRowSelectionChange({
22
- rowKey: rowKey,
23
- checked: !checked,
24
- isShiftClick: false,
25
- });
26
- }, [onRowSelectionChange, rowKey, checked]);
27
-
28
- // Extract the row number from the key (last segment after the final comma)
29
- const rowNumber = useMemo(() => {
30
- const parts = rowKey.split(",");
31
- return Number(parts[parts.length - 1]) + 1;
32
- }, [rowKey]);
33
-
34
- return (
35
- <Pressable
36
- onPress={handlePress}
37
- style={{
38
- paddingHorizontal: 6,
39
- alignItems: "center",
40
- flexDirection: "row",
41
- height: "100%",
42
- cursor: "pointer",
43
- }}
44
- >
45
- {({ hovered }) =>
46
- hovered || checked ? (
47
- <Checkbox checked={checked} />
48
- ) : (
49
- <View style={{ width: 24, alignItems: "center" }}>
50
- <Text color="zinc-500" size="xs" userSelect="none">
51
- {rowNumber}
52
- </Text>
53
- </View>
54
- )
55
- }
56
- </Pressable>
57
- );
58
- });