@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.
- package/package.json +2 -15
- package/src/react_native.d.ts +2 -2
- package/src/segmented_control.tsx +201 -0
- package/src/cell_date.tsx +0 -30
- package/src/cell_date_format.test.ts +0 -32
- package/src/cell_date_format.ts +0 -73
- package/src/cell_number.test.ts +0 -42
- package/src/cell_number.tsx +0 -25
- package/src/cell_number_format.ts +0 -42
- package/src/cell_select.tsx +0 -68
- package/src/cell_text.tsx +0 -45
- package/src/grid/data_grid.tsx +0 -2003
- package/src/grid/data_grid_columns.test.ts +0 -72
- package/src/grid/data_grid_columns.ts +0 -30
- package/src/grid/data_grid_context.ts +0 -119
- package/src/grid/dispatch_safely.ts +0 -39
- package/src/grid/engine.module.css +0 -114
- package/src/grid/engine.tsx +0 -1042
- package/src/grid/helpers.ts +0 -205
- package/src/grid/layout.test.ts +0 -515
- package/src/grid/layout.ts +0 -425
- package/src/grid/recycling.test.ts +0 -236
- package/src/grid/recycling.ts +0 -172
- package/src/grid/row_cell.module.css +0 -105
- package/src/grid/row_cell.tsx +0 -313
- package/src/grid/search_highlight.ts +0 -71
- package/src/grid/select_cell.tsx +0 -58
- package/src/grid/select_group_summary_cell.tsx +0 -76
- package/src/grid/select_header_cell.tsx +0 -32
- package/src/grid/skeleton_row.module.css +0 -34
- package/src/grid/skeleton_row.tsx +0 -20
- package/src/grid/use_grid_groups.ts +0 -311
- package/src/grid/use_scroll_to_cell.ts +0 -135
- package/src/grid/use_virtual_grid.ts +0 -383
- package/src/grid/visibility.test.ts +0 -208
- package/src/grid/visibility.ts +0 -77
- package/src/kanban/constants.ts +0 -18
- package/src/kanban/default_renderers.tsx +0 -160
- package/src/kanban/drag_preview.tsx +0 -157
- package/src/kanban/index.ts +0 -13
- package/src/kanban/insert_card_zone.tsx +0 -135
- package/src/kanban/kanban_board.tsx +0 -635
- package/src/kanban/kanban_card.tsx +0 -321
- package/src/kanban/kanban_column.tsx +0 -499
- package/src/kanban/placeholders.tsx +0 -54
- package/src/kanban/types.ts +0 -116
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { deriveHasSelectColumn, buildGridColumns } from "./data_grid_columns";
|
|
2
|
-
import type { DataGridColumn } from "./data_grid";
|
|
3
|
-
import type { RowPathKey } from "./layout";
|
|
4
|
-
|
|
5
|
-
interface Row {
|
|
6
|
-
name: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const userColumns: DataGridColumn<Row>[] = [
|
|
10
|
-
{ key: "name", name: "Name" },
|
|
11
|
-
{ key: "status", name: "Status" },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const selectColumn: DataGridColumn<Row> = { key: "select", name: "" };
|
|
15
|
-
|
|
16
|
-
const noop = (_rows: Set<RowPathKey>): void => {};
|
|
17
|
-
|
|
18
|
-
// ─── deriveHasSelectColumn ────────────────────────────────────────────────
|
|
19
|
-
// The select column exists only when the consumer participates in row
|
|
20
|
-
// selection — by supplying `selectedRows`, `onSelectedRowsChange`, or both.
|
|
21
|
-
|
|
22
|
-
test("deriveHasSelectColumn - false when selection is unwired", () => {
|
|
23
|
-
expect(deriveHasSelectColumn(undefined, undefined)).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("deriveHasSelectColumn - true when onSelectedRowsChange is provided", () => {
|
|
27
|
-
expect(deriveHasSelectColumn(undefined, noop)).toBe(true);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("deriveHasSelectColumn - true when only selectedRows is provided (read-only display)", () => {
|
|
31
|
-
expect(deriveHasSelectColumn(new Set<RowPathKey>(), undefined)).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("deriveHasSelectColumn - true when both props are provided", () => {
|
|
35
|
-
expect(deriveHasSelectColumn(new Set<RowPathKey>(["0"]), noop)).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// ─── buildGridColumns ─────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
test("buildGridColumns - prepends the select column when selection is wired", () => {
|
|
41
|
-
const columns = buildGridColumns(true, selectColumn, userColumns);
|
|
42
|
-
|
|
43
|
-
expect(columns).toHaveLength(3);
|
|
44
|
-
expect(columns[0].key).toBe("select");
|
|
45
|
-
expect(columns[1].key).toBe("name");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("buildGridColumns - omits the select column when selection is unwired", () => {
|
|
49
|
-
const columns = buildGridColumns(false, selectColumn, userColumns);
|
|
50
|
-
|
|
51
|
-
expect(columns).toHaveLength(2);
|
|
52
|
-
// The first user column sits at index 0 — no inert checkbox column ahead of it.
|
|
53
|
-
expect(columns[0].key).toBe("name");
|
|
54
|
-
expect(columns.some((c) => c.key === "select")).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("buildGridColumns - read-only selection (selectedRows only) still renders the select column", () => {
|
|
58
|
-
const hasSelectColumn = deriveHasSelectColumn(new Set<RowPathKey>(["0"]), undefined);
|
|
59
|
-
const columns = buildGridColumns(hasSelectColumn, selectColumn, userColumns);
|
|
60
|
-
|
|
61
|
-
expect(columns[0].key).toBe("select");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("buildGridColumns - does not mutate the input userColumns array", () => {
|
|
65
|
-
const input: DataGridColumn<Row>[] = [{ key: "name", name: "Name" }];
|
|
66
|
-
const before = [...input];
|
|
67
|
-
|
|
68
|
-
buildGridColumns(true, selectColumn, input);
|
|
69
|
-
buildGridColumns(false, selectColumn, input);
|
|
70
|
-
|
|
71
|
-
expect(input).toEqual(before);
|
|
72
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { RowPathKey } from "@lotics/ui/grid/layout";
|
|
2
|
-
import type { DataGridColumn } from "./data_grid";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Whether the grid renders a select (checkbox) column. Row selection is fully
|
|
6
|
-
* controlled: the column is interactive only when the consumer supplies
|
|
7
|
-
* `selectedRows` (to display selection) or `onSelectedRowsChange` (to mutate
|
|
8
|
-
* it). With neither, the column would be inert — uncheckable and unobservable —
|
|
9
|
-
* so it must not exist or occupy layout space.
|
|
10
|
-
*/
|
|
11
|
-
export function deriveHasSelectColumn(
|
|
12
|
-
selectedRows: Set<RowPathKey> | undefined,
|
|
13
|
-
onSelectedRowsChange: ((selectedRows: Set<RowPathKey>) => void) | undefined,
|
|
14
|
-
): boolean {
|
|
15
|
-
return selectedRows !== undefined || onSelectedRowsChange !== undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Assemble the grid's column list. The select column is prepended only when
|
|
20
|
-
* `hasSelectColumn` is true (see `deriveHasSelectColumn`).
|
|
21
|
-
*
|
|
22
|
-
* Pure: returns a new array, never mutates `userColumns`.
|
|
23
|
-
*/
|
|
24
|
-
export function buildGridColumns<TRow>(
|
|
25
|
-
hasSelectColumn: boolean,
|
|
26
|
-
selectColumn: DataGridColumn<TRow>,
|
|
27
|
-
userColumns: DataGridColumn<TRow>[],
|
|
28
|
-
): DataGridColumn<TRow>[] {
|
|
29
|
-
return hasSelectColumn ? [selectColumn, ...userColumns] : [...userColumns];
|
|
30
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
2
|
-
import type { RowPathKey } from "@lotics/ui/grid/layout";
|
|
3
|
-
|
|
4
|
-
export type RowId = string;
|
|
5
|
-
|
|
6
|
-
// ─── Cell animation hook — optional, default returns false ────────────────
|
|
7
|
-
//
|
|
8
|
-
// Records page wires this to useRealtimeChangeAnimation(tableId, …) so remote
|
|
9
|
-
// updates flash matching cells. Iframe apps leave it unset; row_cell always
|
|
10
|
-
// calls the contextualized hook, so React's hook-order stays stable.
|
|
11
|
-
|
|
12
|
-
export type UseCellAnimationFn = (rowId: RowId, columnKey: string) => boolean;
|
|
13
|
-
|
|
14
|
-
/** Default no-op when consumer doesn't supply useCellAnimation. Module-level
|
|
15
|
-
* so its identity is stable across renders. Exported so DataGrid can wire it
|
|
16
|
-
* into its Provider without redeclaring. */
|
|
17
|
-
export const noopUseCellAnimation: UseCellAnimationFn = () => false;
|
|
18
|
-
|
|
19
|
-
export const CellAnimationContext = createContext<UseCellAnimationFn>(noopUseCellAnimation);
|
|
20
|
-
|
|
21
|
-
export function useCellAnimation(rowId: RowId, columnKey: string): boolean {
|
|
22
|
-
const fn = useContext(CellAnimationContext);
|
|
23
|
-
return fn(rowId, columnKey);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface RowSelectionContextValue {
|
|
27
|
-
selectedRows?: Set<RowPathKey>;
|
|
28
|
-
onSelectedRowsChange?: (selectedRows: Set<RowPathKey>) => void;
|
|
29
|
-
allRowPathKeys: Set<RowPathKey>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const RowSelectionContext = createContext<RowSelectionContextValue | null>(null);
|
|
33
|
-
|
|
34
|
-
export function useRowSelection() {
|
|
35
|
-
const context = useContext(RowSelectionContext);
|
|
36
|
-
if (!context) {
|
|
37
|
-
throw new Error("useRowSelection must be used within a DataGrid");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const { selectedRows, onSelectedRowsChange } = context;
|
|
41
|
-
|
|
42
|
-
const isRowSelected = useCallback(
|
|
43
|
-
(rowKey: RowPathKey) => {
|
|
44
|
-
return selectedRows?.has(rowKey) ?? false;
|
|
45
|
-
},
|
|
46
|
-
[selectedRows],
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const onRowSelectionChange = useCallback(
|
|
50
|
-
(params: { rowKey: RowPathKey; checked: boolean; isShiftClick: boolean }) => {
|
|
51
|
-
if (!onSelectedRowsChange) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const newSelectedRows = new Set(selectedRows ?? []);
|
|
56
|
-
|
|
57
|
-
if (params.checked) {
|
|
58
|
-
newSelectedRows.add(params.rowKey);
|
|
59
|
-
} else {
|
|
60
|
-
newSelectedRows.delete(params.rowKey);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
onSelectedRowsChange(newSelectedRows);
|
|
64
|
-
},
|
|
65
|
-
[selectedRows, onSelectedRowsChange],
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
isRowSelected,
|
|
70
|
-
onRowSelectionChange,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function useHeaderRowSelection() {
|
|
75
|
-
const context = useContext(RowSelectionContext);
|
|
76
|
-
if (!context) {
|
|
77
|
-
throw new Error("useHeaderRowSelection must be used within a DataGrid");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const { selectedRows, onSelectedRowsChange, allRowPathKeys } = context;
|
|
81
|
-
|
|
82
|
-
const selectedCount = useMemo(() => {
|
|
83
|
-
if (!selectedRows || !allRowPathKeys || !allRowPathKeys.size) {
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
86
|
-
return Array.from(allRowPathKeys).filter((key) => selectedRows.has(key)).length;
|
|
87
|
-
}, [selectedRows, allRowPathKeys]);
|
|
88
|
-
|
|
89
|
-
const isIndeterminate = useMemo(
|
|
90
|
-
() => selectedCount > 0 && selectedCount < (allRowPathKeys?.size ?? 0),
|
|
91
|
-
[selectedCount, allRowPathKeys?.size],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const isRowSelected = useMemo(
|
|
95
|
-
() => selectedCount === (allRowPathKeys?.size ?? 0) && (allRowPathKeys?.size ?? 0) > 0,
|
|
96
|
-
[selectedCount, allRowPathKeys?.size],
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const onRowSelectionChange = useCallback(
|
|
100
|
-
(params: { checked: boolean }) => {
|
|
101
|
-
if (!onSelectedRowsChange) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (params.checked) {
|
|
106
|
-
onSelectedRowsChange(new Set(allRowPathKeys));
|
|
107
|
-
} else {
|
|
108
|
-
onSelectedRowsChange(new Set());
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
[onSelectedRowsChange, allRowPathKeys],
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
isIndeterminate,
|
|
116
|
-
isRowSelected,
|
|
117
|
-
onRowSelectionChange,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pub-sub dispatcher with hard isolation between subscribers.
|
|
3
|
-
*
|
|
4
|
-
* Contract:
|
|
5
|
-
* 1. Every item in `items` is invoked exactly once per call.
|
|
6
|
-
* 2. A throwing invocation never stops later items from being invoked.
|
|
7
|
-
* 3. A throwing invocation never propagates back to the dispatcher's caller.
|
|
8
|
-
*
|
|
9
|
-
* Subscriber exceptions are re-raised to the host error handler
|
|
10
|
-
* (`globalThis.reportError`, falling back to a fresh task) so they surface
|
|
11
|
-
* via `window.onerror` with full original stack. This is NOT a swallow:
|
|
12
|
-
* the error is fully reported, loudly, with context — same mechanism the
|
|
13
|
-
* HTML spec uses for EventTarget.dispatchEvent to "report the exception"
|
|
14
|
-
* without joining listener failures to the dispatcher's call stack.
|
|
15
|
-
*
|
|
16
|
-
* Vendored from @lotics/ui-internal/dispatch_safely (workspace-private).
|
|
17
|
-
* Kept inline in @lotics/ui/grid so the grid module can be published
|
|
18
|
-
* without a dependency on the internal package.
|
|
19
|
-
*/
|
|
20
|
-
export function dispatchSafely<T>(items: Iterable<T>, invoke: (item: T) => void): void {
|
|
21
|
-
for (const item of items) {
|
|
22
|
-
try {
|
|
23
|
-
invoke(item);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
reportListenerError(error);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function reportListenerError(error: unknown): void {
|
|
31
|
-
const globalReportError = (globalThis as { reportError?: (error: unknown) => void }).reportError;
|
|
32
|
-
if (typeof globalReportError === "function") {
|
|
33
|
-
globalReportError(error);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
setTimeout(() => {
|
|
37
|
-
throw error;
|
|
38
|
-
}, 0);
|
|
39
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
.root {
|
|
2
|
-
position: relative;
|
|
3
|
-
overflow: scroll;
|
|
4
|
-
will-change: scroll-position;
|
|
5
|
-
height: 100%;
|
|
6
|
-
width: 100%;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.content {
|
|
10
|
-
position: relative;
|
|
11
|
-
transform: translateZ(0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.rows_wrapper {
|
|
15
|
-
position: absolute;
|
|
16
|
-
transform: translateZ(0);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
.row_wrapper {
|
|
20
|
-
position: absolute;
|
|
21
|
-
top: 0;
|
|
22
|
-
left: 0;
|
|
23
|
-
display: flex;
|
|
24
|
-
flex-direction: row;
|
|
25
|
-
will-change: transform;
|
|
26
|
-
content-visibility: auto;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.row {
|
|
30
|
-
position: relative;
|
|
31
|
-
display: flex;
|
|
32
|
-
flex-direction: row;
|
|
33
|
-
flex-shrink: 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.frozen_cells {
|
|
37
|
-
position: sticky;
|
|
38
|
-
left: 0;
|
|
39
|
-
z-index: 3;
|
|
40
|
-
display: flex;
|
|
41
|
-
flex-direction: row;
|
|
42
|
-
flex-shrink: 0;
|
|
43
|
-
box-sizing: border-box;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.frozen_cells::after {
|
|
47
|
-
content: "";
|
|
48
|
-
position: absolute;
|
|
49
|
-
top: 0;
|
|
50
|
-
right: -6px;
|
|
51
|
-
width: 6px;
|
|
52
|
-
height: 100%;
|
|
53
|
-
background: linear-gradient(to right, rgba(0, 0, 0, 0.06), transparent);
|
|
54
|
-
pointer-events: none;
|
|
55
|
-
opacity: 0;
|
|
56
|
-
transition: opacity 0.15s ease-out;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* Show shadow only when grid is scrolled horizontally */
|
|
60
|
-
.root[data-scrolled-x] .frozen_cells::after {
|
|
61
|
-
opacity: 1;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.scrollable_cells {
|
|
65
|
-
position: relative;
|
|
66
|
-
flex-shrink: 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.cell {
|
|
70
|
-
display: flex;
|
|
71
|
-
flex-direction: column;
|
|
72
|
-
flex-shrink: 0;
|
|
73
|
-
contain: layout style paint;
|
|
74
|
-
box-sizing: border-box;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.header_wrapper {
|
|
78
|
-
position: sticky;
|
|
79
|
-
top: 0;
|
|
80
|
-
z-index: 4;
|
|
81
|
-
display: flex;
|
|
82
|
-
flex-direction: row;
|
|
83
|
-
will-change: transform;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.footer_wrapper {
|
|
87
|
-
position: sticky;
|
|
88
|
-
bottom: 0;
|
|
89
|
-
display: flex;
|
|
90
|
-
flex-direction: row;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.group_heading_wrapper {
|
|
94
|
-
position: absolute;
|
|
95
|
-
top: 0;
|
|
96
|
-
display: flex;
|
|
97
|
-
flex-direction: row;
|
|
98
|
-
will-change: transform;
|
|
99
|
-
content-visibility: auto;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.group_heading_content {
|
|
103
|
-
position: sticky;
|
|
104
|
-
left: 0px;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.spacer {
|
|
108
|
-
position: absolute;
|
|
109
|
-
top: 0;
|
|
110
|
-
left: 0;
|
|
111
|
-
width: 100%;
|
|
112
|
-
will-change: transform;
|
|
113
|
-
content-visibility: auto;
|
|
114
|
-
}
|