@ai-matrx/design-system 0.22.0 → 0.26.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/CHANGELOG.md +227 -0
- package/dist/data-table/cell-undo.cjs +98 -0
- package/dist/data-table/cell-undo.cjs.map +1 -0
- package/dist/data-table/cell-undo.d.cts +85 -0
- package/dist/data-table/cell-undo.d.ts +85 -0
- package/dist/data-table/cell-undo.js +78 -0
- package/dist/data-table/cell-undo.js.map +1 -0
- package/dist/data-table/copy-helpers.d.cts +7 -2
- package/dist/data-table/copy-helpers.d.ts +7 -2
- package/dist/data-table/coverage.cjs +116 -0
- package/dist/data-table/coverage.cjs.map +1 -0
- package/dist/data-table/coverage.d.cts +65 -0
- package/dist/data-table/coverage.d.ts +65 -0
- package/dist/data-table/coverage.js +96 -0
- package/dist/data-table/coverage.js.map +1 -0
- package/dist/data-table/data-row-inspector.cjs.map +1 -1
- package/dist/data-table/data-row-inspector.js.map +1 -1
- package/dist/data-table/data-row-window.cjs.map +1 -1
- package/dist/data-table/data-row-window.js.map +1 -1
- package/dist/data-table/facets.cjs +118 -0
- package/dist/data-table/facets.cjs.map +1 -0
- package/dist/data-table/facets.d.cts +100 -0
- package/dist/data-table/facets.d.ts +100 -0
- package/dist/data-table/facets.js +98 -0
- package/dist/data-table/facets.js.map +1 -0
- package/dist/data-table/fill.cjs +299 -0
- package/dist/data-table/fill.cjs.map +1 -0
- package/dist/data-table/fill.d.cts +129 -0
- package/dist/data-table/fill.d.ts +129 -0
- package/dist/data-table/fill.js +277 -0
- package/dist/data-table/fill.js.map +1 -0
- package/dist/data-table/filter-engine.d.cts +7 -2
- package/dist/data-table/filter-engine.d.ts +7 -2
- package/dist/data-table/grid-clipboard.cjs +165 -0
- package/dist/data-table/grid-clipboard.cjs.map +1 -0
- package/dist/data-table/grid-clipboard.d.cts +85 -0
- package/dist/data-table/grid-clipboard.d.ts +85 -0
- package/dist/data-table/grid-clipboard.js +145 -0
- package/dist/data-table/grid-clipboard.js.map +1 -0
- package/dist/data-table/grid-selection.cjs +258 -0
- package/dist/data-table/grid-selection.cjs.map +1 -0
- package/dist/data-table/grid-selection.d.cts +166 -0
- package/dist/data-table/grid-selection.d.ts +166 -0
- package/dist/data-table/grid-selection.js +238 -0
- package/dist/data-table/grid-selection.js.map +1 -0
- package/dist/data-table/grouping.cjs +302 -0
- package/dist/data-table/grouping.cjs.map +1 -0
- package/dist/data-table/grouping.d.cts +119 -0
- package/dist/data-table/grouping.d.ts +119 -0
- package/dist/data-table/grouping.js +282 -0
- package/dist/data-table/grouping.js.map +1 -0
- package/dist/data-table/host.cjs.map +1 -1
- package/dist/data-table/host.d.cts +18 -4
- package/dist/data-table/host.d.ts +18 -4
- package/dist/data-table/host.js.map +1 -1
- package/dist/data-table/index.cjs +3173 -451
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +13 -3
- package/dist/data-table/index.d.ts +13 -3
- package/dist/data-table/index.js +3174 -451
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +7 -2
- package/dist/data-table/infer-filter.d.ts +7 -2
- package/dist/data-table/layered-filters.d.cts +7 -2
- package/dist/data-table/layered-filters.d.ts +7 -2
- package/dist/data-table/menu-targets.cjs +77 -0
- package/dist/data-table/menu-targets.cjs.map +1 -0
- package/dist/data-table/menu-targets.d.cts +108 -0
- package/dist/data-table/menu-targets.d.ts +108 -0
- package/dist/data-table/menu-targets.js +57 -0
- package/dist/data-table/menu-targets.js.map +1 -0
- package/dist/data-table/pagination.cjs.map +1 -1
- package/dist/data-table/pagination.js.map +1 -1
- package/dist/data-table/query-control.d.cts +7 -2
- package/dist/data-table/query-control.d.ts +7 -2
- package/dist/data-table/spreadsheet.cjs +1062 -0
- package/dist/data-table/spreadsheet.cjs.map +1 -0
- package/dist/data-table/spreadsheet.d.cts +153 -0
- package/dist/data-table/spreadsheet.d.ts +153 -0
- package/dist/data-table/spreadsheet.js +1042 -0
- package/dist/data-table/spreadsheet.js.map +1 -0
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +7 -2
- package/dist/data-table/types.d.ts +7 -2
- package/dist/data-table/url-state.d.cts +7 -2
- package/dist/data-table/url-state.d.ts +7 -2
- package/dist/data-table/uuid-cell.cjs.map +1 -1
- package/dist/data-table/uuid-cell.js.map +1 -1
- package/dist/data-table/validation.cjs +229 -0
- package/dist/data-table/validation.cjs.map +1 -0
- package/dist/data-table/validation.d.cts +125 -0
- package/dist/data-table/validation.d.ts +125 -0
- package/dist/data-table/validation.js +209 -0
- package/dist/data-table/validation.js.map +1 -0
- package/dist/data-table/virtual-window.cjs +203 -0
- package/dist/data-table/virtual-window.cjs.map +1 -0
- package/dist/data-table/virtual-window.d.cts +84 -0
- package/dist/data-table/virtual-window.d.ts +84 -0
- package/dist/data-table/virtual-window.js +183 -0
- package/dist/data-table/virtual-window.js.map +1 -0
- package/dist/data-table/xlsx.d.cts +7 -2
- package/dist/data-table/xlsx.d.ts +7 -2
- package/dist/field-formats/index.d.cts +2 -2
- package/dist/field-formats/index.d.ts +2 -2
- package/dist/formulas/index.cjs +1041 -0
- package/dist/formulas/index.cjs.map +1 -0
- package/dist/formulas/index.d.cts +389 -0
- package/dist/formulas/index.d.ts +389 -0
- package/dist/formulas/index.js +1019 -0
- package/dist/formulas/index.js.map +1 -0
- package/dist/{types-BdbvlF9-.d.cts → types-CJWU1Gae.d.cts} +1 -1
- package/dist/{types-BdbvlF9-.d.ts → types-CJWU1Gae.d.ts} +1 -1
- package/dist/{types-D-flTpK0.d.ts → types-Cu5_eUG1.d.ts} +268 -4
- package/dist/{types-Sn249eoy.d.cts → types-DrJXAAoD.d.cts} +268 -4
- package/package.json +129 -2
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid selection model for user data tables.
|
|
3
|
+
* @ai-matrx/design-system/data-table/grid-selection
|
|
4
|
+
*
|
|
5
|
+
* This moved here from the app so every table in the product shares one
|
|
6
|
+
* range-selection model.
|
|
7
|
+
*
|
|
8
|
+
* THE THREE STATES. A spreadsheet grid has three, not two, and collapsing them
|
|
9
|
+
* to two is what made this grid feel like a web page instead of a tool:
|
|
10
|
+
*
|
|
11
|
+
* 1. NOTHING SELECTED — the grid at rest.
|
|
12
|
+
* 2. SELECTED — one cell is current. Nothing has changed. You can
|
|
13
|
+
* look, copy, and move with the keyboard.
|
|
14
|
+
* 3. EDITING — a value is being changed.
|
|
15
|
+
*
|
|
16
|
+
* State 2 is the one that was missing, and its absence is why every edit needed
|
|
17
|
+
* a double-click and why arrow keys, Tab, copy and fill-down were all
|
|
18
|
+
* impossible: none of them mean anything without a notion of "the current cell".
|
|
19
|
+
*
|
|
20
|
+
* 🚨 THE CLICK LAW — a single click may SELECT a cell, TOGGLE a two-state
|
|
21
|
+
* value, or OPEN a chooser. It may NEVER drop the user into a free-text buffer.
|
|
22
|
+
* Opening a menu is not a mutation and toggling a checkbox is instantly
|
|
23
|
+
* reversible, but landing in a text buffer turns every attempt to select-and-
|
|
24
|
+
* copy into an accidental edit. `directClickKinds()` below is the whole list of
|
|
25
|
+
* what a single click may act on; adding a free-text editor to it is a defect.
|
|
26
|
+
*
|
|
27
|
+
* A CHOOSER OPENS ON THE SECOND CLICK. A choice cell's first click SELECTS it
|
|
28
|
+
* (so Cmd-C, Cmd-X, the arrow keys and the right-click menu all have a current
|
|
29
|
+
* cell); a click on the cell that is ALREADY selected opens the option list —
|
|
30
|
+
* the Airtable gesture. Opening the chooser on the very first click moved
|
|
31
|
+
* focus into its search box, and every keyboard shortcut then went to that box
|
|
32
|
+
* instead of the grid: a choice column could not be copied at all (2026-09-14).
|
|
33
|
+
* Checkboxes and ratings stay one-click: a mis-click on those is instantly
|
|
34
|
+
* undone in place and steals no focus.
|
|
35
|
+
*
|
|
36
|
+
* CLIPBOARD KEYS. Cmd-C / Cmd-X / Cmd-V on a selected cell act on THAT CELL —
|
|
37
|
+
* copy its value, cut it (copy + clear), paste over it. A paste carrying a
|
|
38
|
+
* spreadsheet block (tabs / line breaks) lands as a block from the selected
|
|
39
|
+
* cell downward and rightward (`grid-clipboard.ts`). None of these need an
|
|
40
|
+
* editor open, which is the whole point: a normal table copies what you point
|
|
41
|
+
* at, not only what you are typing into.
|
|
42
|
+
*
|
|
43
|
+
* Addresses are (rowId, fieldName), never (rowIndex, colIndex): the grid
|
|
44
|
+
* reloads after every write, and realtime reorders rows underneath the user. An
|
|
45
|
+
* index-based selection silently jumps to a DIFFERENT cell when that happens —
|
|
46
|
+
* and the next keystroke then edits the wrong row.
|
|
47
|
+
*
|
|
48
|
+
* Pure module: no React, no DOM. Everything here is a function of the current
|
|
49
|
+
* address and the row/field lists, which is what makes the navigation testable
|
|
50
|
+
* without mounting a grid.
|
|
51
|
+
*/
|
|
52
|
+
type CellAddress = {
|
|
53
|
+
rowId: string;
|
|
54
|
+
fieldName: string;
|
|
55
|
+
};
|
|
56
|
+
type GridMove = "up" | "down" | "left" | "right" | "nextCell" | "prevCell" | "rowStart" | "rowEnd" | "gridStart" | "gridEnd";
|
|
57
|
+
/** What a keystroke means when a cell is selected but NOT being edited. */
|
|
58
|
+
type GridKeyAction = {
|
|
59
|
+
kind: "move";
|
|
60
|
+
move: GridMove;
|
|
61
|
+
}
|
|
62
|
+
/** Shift+arrow: grow the range toward `move`, anchor fixed. */
|
|
63
|
+
| {
|
|
64
|
+
kind: "extend";
|
|
65
|
+
move: GridMove;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "selectAll";
|
|
68
|
+
} | {
|
|
69
|
+
kind: "selectRow";
|
|
70
|
+
} | {
|
|
71
|
+
kind: "selectColumn";
|
|
72
|
+
} | {
|
|
73
|
+
kind: "fillDown";
|
|
74
|
+
} | {
|
|
75
|
+
kind: "edit";
|
|
76
|
+
} | {
|
|
77
|
+
kind: "editSeeded";
|
|
78
|
+
seed: string;
|
|
79
|
+
} | {
|
|
80
|
+
kind: "clearCell";
|
|
81
|
+
} | {
|
|
82
|
+
kind: "copy";
|
|
83
|
+
} | {
|
|
84
|
+
kind: "cut";
|
|
85
|
+
} | {
|
|
86
|
+
kind: "paste";
|
|
87
|
+
} | {
|
|
88
|
+
kind: "escape";
|
|
89
|
+
} | null;
|
|
90
|
+
type CellRange = {
|
|
91
|
+
anchor: CellAddress;
|
|
92
|
+
focus: CellAddress;
|
|
93
|
+
};
|
|
94
|
+
/** The rectangle a range spans, as inclusive indexes into the grid's order. */
|
|
95
|
+
type RangeBounds = {
|
|
96
|
+
r0: number;
|
|
97
|
+
r1: number;
|
|
98
|
+
c0: number;
|
|
99
|
+
c1: number;
|
|
100
|
+
};
|
|
101
|
+
declare function rangeBounds(range: CellRange, rowIds: readonly string[], fieldNames: readonly string[]): RangeBounds | null;
|
|
102
|
+
declare function rangeSize(bounds: RangeBounds): {
|
|
103
|
+
rows: number;
|
|
104
|
+
cols: number;
|
|
105
|
+
cells: number;
|
|
106
|
+
};
|
|
107
|
+
declare function boundsContain(bounds: RangeBounds, rowIndex: number, colIndex: number): boolean;
|
|
108
|
+
/** Every cell of the range in reading order (row by row, left to right). */
|
|
109
|
+
declare function cellsInRange(range: CellRange, rowIds: readonly string[], fieldNames: readonly string[]): CellAddress[];
|
|
110
|
+
/** The range as a 2-D grid of addresses — what a block copy serialises. */
|
|
111
|
+
declare function rangeRows(range: CellRange, rowIds: readonly string[], fieldNames: readonly string[]): CellAddress[][];
|
|
112
|
+
declare function isSingleCellRange(range: CellRange): boolean;
|
|
113
|
+
/** The whole row, first column to last — what clicking a row's edge selects. */
|
|
114
|
+
declare function rowRange(rowId: string, fieldNames: readonly string[]): CellRange | null;
|
|
115
|
+
/** The whole column across the page — what clicking a header's edge selects. */
|
|
116
|
+
declare function columnRange(fieldName: string, rowIds: readonly string[]): CellRange | null;
|
|
117
|
+
/** Every cell on the page. */
|
|
118
|
+
declare function allRange(rowIds: readonly string[], fieldNames: readonly string[]): CellRange | null;
|
|
119
|
+
declare function sameCell(a: CellAddress | null, b: CellAddress | null): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Where does `move` land from `current`?
|
|
122
|
+
*
|
|
123
|
+
* Returns null when the move is impossible (no selection, empty grid). Vertical
|
|
124
|
+
* moves CLAMP at the edges — running into the top of the grid should park you
|
|
125
|
+
* on the first row, not clear the selection, because a selection that vanishes
|
|
126
|
+
* when you overshoot loses your place. Horizontal Tab moves WRAP to the next or
|
|
127
|
+
* previous row, which is what makes Tab a usable data-entry gesture.
|
|
128
|
+
*/
|
|
129
|
+
declare function moveSelection(current: CellAddress | null, move: GridMove, rowIds: readonly string[], fieldNames: readonly string[]): CellAddress | null;
|
|
130
|
+
/**
|
|
131
|
+
* A single printable character typed on a selected cell starts an edit seeded
|
|
132
|
+
* with it — the spreadsheet reflex of "just start typing to replace".
|
|
133
|
+
*
|
|
134
|
+
* Modifier combinations are excluded so Cmd-C / Ctrl-R never get mistaken for
|
|
135
|
+
* typing, and only genuinely printable single characters qualify: `key` is the
|
|
136
|
+
* character itself for those, and a multi-character name ("ArrowUp", "F3") for
|
|
137
|
+
* everything else.
|
|
138
|
+
*/
|
|
139
|
+
declare function isTypingKey(e: {
|
|
140
|
+
key: string;
|
|
141
|
+
ctrlKey: boolean;
|
|
142
|
+
metaKey: boolean;
|
|
143
|
+
altKey: boolean;
|
|
144
|
+
}): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Translate a keystroke into a grid action. Called ONLY when a cell is selected
|
|
147
|
+
* and not being edited — an editor owns its own keys while it is open.
|
|
148
|
+
*/
|
|
149
|
+
declare function classifyGridKey(e: {
|
|
150
|
+
key: string;
|
|
151
|
+
shiftKey: boolean;
|
|
152
|
+
ctrlKey: boolean;
|
|
153
|
+
metaKey: boolean;
|
|
154
|
+
altKey: boolean;
|
|
155
|
+
}): GridKeyAction;
|
|
156
|
+
/**
|
|
157
|
+
* Editor kinds a SINGLE click may operate directly, without first entering edit
|
|
158
|
+
* mode. See THE CLICK LAW at the top of this file — this list is closed sets
|
|
159
|
+
* and two-state values only, where a mis-click is obvious and instantly undone.
|
|
160
|
+
*/
|
|
161
|
+
declare function directClickKinds(): readonly string[];
|
|
162
|
+
declare function isDirectClickEditor(editorKind: string | undefined): boolean;
|
|
163
|
+
/** Stable DOM key for a cell, so selection can scroll itself into view. */
|
|
164
|
+
declare function cellDomKey(address: CellAddress): string;
|
|
165
|
+
|
|
166
|
+
export { type CellAddress, type CellRange, type GridKeyAction, type GridMove, type RangeBounds, allRange, boundsContain, cellDomKey, cellsInRange, classifyGridKey, columnRange, directClickKinds, isDirectClickEditor, isSingleCellRange, isTypingKey, moveSelection, rangeBounds, rangeRows, rangeSize, rowRange, sameCell };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid selection model for user data tables.
|
|
3
|
+
* @ai-matrx/design-system/data-table/grid-selection
|
|
4
|
+
*
|
|
5
|
+
* This moved here from the app so every table in the product shares one
|
|
6
|
+
* range-selection model.
|
|
7
|
+
*
|
|
8
|
+
* THE THREE STATES. A spreadsheet grid has three, not two, and collapsing them
|
|
9
|
+
* to two is what made this grid feel like a web page instead of a tool:
|
|
10
|
+
*
|
|
11
|
+
* 1. NOTHING SELECTED — the grid at rest.
|
|
12
|
+
* 2. SELECTED — one cell is current. Nothing has changed. You can
|
|
13
|
+
* look, copy, and move with the keyboard.
|
|
14
|
+
* 3. EDITING — a value is being changed.
|
|
15
|
+
*
|
|
16
|
+
* State 2 is the one that was missing, and its absence is why every edit needed
|
|
17
|
+
* a double-click and why arrow keys, Tab, copy and fill-down were all
|
|
18
|
+
* impossible: none of them mean anything without a notion of "the current cell".
|
|
19
|
+
*
|
|
20
|
+
* 🚨 THE CLICK LAW — a single click may SELECT a cell, TOGGLE a two-state
|
|
21
|
+
* value, or OPEN a chooser. It may NEVER drop the user into a free-text buffer.
|
|
22
|
+
* Opening a menu is not a mutation and toggling a checkbox is instantly
|
|
23
|
+
* reversible, but landing in a text buffer turns every attempt to select-and-
|
|
24
|
+
* copy into an accidental edit. `directClickKinds()` below is the whole list of
|
|
25
|
+
* what a single click may act on; adding a free-text editor to it is a defect.
|
|
26
|
+
*
|
|
27
|
+
* A CHOOSER OPENS ON THE SECOND CLICK. A choice cell's first click SELECTS it
|
|
28
|
+
* (so Cmd-C, Cmd-X, the arrow keys and the right-click menu all have a current
|
|
29
|
+
* cell); a click on the cell that is ALREADY selected opens the option list —
|
|
30
|
+
* the Airtable gesture. Opening the chooser on the very first click moved
|
|
31
|
+
* focus into its search box, and every keyboard shortcut then went to that box
|
|
32
|
+
* instead of the grid: a choice column could not be copied at all (2026-09-14).
|
|
33
|
+
* Checkboxes and ratings stay one-click: a mis-click on those is instantly
|
|
34
|
+
* undone in place and steals no focus.
|
|
35
|
+
*
|
|
36
|
+
* CLIPBOARD KEYS. Cmd-C / Cmd-X / Cmd-V on a selected cell act on THAT CELL —
|
|
37
|
+
* copy its value, cut it (copy + clear), paste over it. A paste carrying a
|
|
38
|
+
* spreadsheet block (tabs / line breaks) lands as a block from the selected
|
|
39
|
+
* cell downward and rightward (`grid-clipboard.ts`). None of these need an
|
|
40
|
+
* editor open, which is the whole point: a normal table copies what you point
|
|
41
|
+
* at, not only what you are typing into.
|
|
42
|
+
*
|
|
43
|
+
* Addresses are (rowId, fieldName), never (rowIndex, colIndex): the grid
|
|
44
|
+
* reloads after every write, and realtime reorders rows underneath the user. An
|
|
45
|
+
* index-based selection silently jumps to a DIFFERENT cell when that happens —
|
|
46
|
+
* and the next keystroke then edits the wrong row.
|
|
47
|
+
*
|
|
48
|
+
* Pure module: no React, no DOM. Everything here is a function of the current
|
|
49
|
+
* address and the row/field lists, which is what makes the navigation testable
|
|
50
|
+
* without mounting a grid.
|
|
51
|
+
*/
|
|
52
|
+
type CellAddress = {
|
|
53
|
+
rowId: string;
|
|
54
|
+
fieldName: string;
|
|
55
|
+
};
|
|
56
|
+
type GridMove = "up" | "down" | "left" | "right" | "nextCell" | "prevCell" | "rowStart" | "rowEnd" | "gridStart" | "gridEnd";
|
|
57
|
+
/** What a keystroke means when a cell is selected but NOT being edited. */
|
|
58
|
+
type GridKeyAction = {
|
|
59
|
+
kind: "move";
|
|
60
|
+
move: GridMove;
|
|
61
|
+
}
|
|
62
|
+
/** Shift+arrow: grow the range toward `move`, anchor fixed. */
|
|
63
|
+
| {
|
|
64
|
+
kind: "extend";
|
|
65
|
+
move: GridMove;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "selectAll";
|
|
68
|
+
} | {
|
|
69
|
+
kind: "selectRow";
|
|
70
|
+
} | {
|
|
71
|
+
kind: "selectColumn";
|
|
72
|
+
} | {
|
|
73
|
+
kind: "fillDown";
|
|
74
|
+
} | {
|
|
75
|
+
kind: "edit";
|
|
76
|
+
} | {
|
|
77
|
+
kind: "editSeeded";
|
|
78
|
+
seed: string;
|
|
79
|
+
} | {
|
|
80
|
+
kind: "clearCell";
|
|
81
|
+
} | {
|
|
82
|
+
kind: "copy";
|
|
83
|
+
} | {
|
|
84
|
+
kind: "cut";
|
|
85
|
+
} | {
|
|
86
|
+
kind: "paste";
|
|
87
|
+
} | {
|
|
88
|
+
kind: "escape";
|
|
89
|
+
} | null;
|
|
90
|
+
type CellRange = {
|
|
91
|
+
anchor: CellAddress;
|
|
92
|
+
focus: CellAddress;
|
|
93
|
+
};
|
|
94
|
+
/** The rectangle a range spans, as inclusive indexes into the grid's order. */
|
|
95
|
+
type RangeBounds = {
|
|
96
|
+
r0: number;
|
|
97
|
+
r1: number;
|
|
98
|
+
c0: number;
|
|
99
|
+
c1: number;
|
|
100
|
+
};
|
|
101
|
+
declare function rangeBounds(range: CellRange, rowIds: readonly string[], fieldNames: readonly string[]): RangeBounds | null;
|
|
102
|
+
declare function rangeSize(bounds: RangeBounds): {
|
|
103
|
+
rows: number;
|
|
104
|
+
cols: number;
|
|
105
|
+
cells: number;
|
|
106
|
+
};
|
|
107
|
+
declare function boundsContain(bounds: RangeBounds, rowIndex: number, colIndex: number): boolean;
|
|
108
|
+
/** Every cell of the range in reading order (row by row, left to right). */
|
|
109
|
+
declare function cellsInRange(range: CellRange, rowIds: readonly string[], fieldNames: readonly string[]): CellAddress[];
|
|
110
|
+
/** The range as a 2-D grid of addresses — what a block copy serialises. */
|
|
111
|
+
declare function rangeRows(range: CellRange, rowIds: readonly string[], fieldNames: readonly string[]): CellAddress[][];
|
|
112
|
+
declare function isSingleCellRange(range: CellRange): boolean;
|
|
113
|
+
/** The whole row, first column to last — what clicking a row's edge selects. */
|
|
114
|
+
declare function rowRange(rowId: string, fieldNames: readonly string[]): CellRange | null;
|
|
115
|
+
/** The whole column across the page — what clicking a header's edge selects. */
|
|
116
|
+
declare function columnRange(fieldName: string, rowIds: readonly string[]): CellRange | null;
|
|
117
|
+
/** Every cell on the page. */
|
|
118
|
+
declare function allRange(rowIds: readonly string[], fieldNames: readonly string[]): CellRange | null;
|
|
119
|
+
declare function sameCell(a: CellAddress | null, b: CellAddress | null): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Where does `move` land from `current`?
|
|
122
|
+
*
|
|
123
|
+
* Returns null when the move is impossible (no selection, empty grid). Vertical
|
|
124
|
+
* moves CLAMP at the edges — running into the top of the grid should park you
|
|
125
|
+
* on the first row, not clear the selection, because a selection that vanishes
|
|
126
|
+
* when you overshoot loses your place. Horizontal Tab moves WRAP to the next or
|
|
127
|
+
* previous row, which is what makes Tab a usable data-entry gesture.
|
|
128
|
+
*/
|
|
129
|
+
declare function moveSelection(current: CellAddress | null, move: GridMove, rowIds: readonly string[], fieldNames: readonly string[]): CellAddress | null;
|
|
130
|
+
/**
|
|
131
|
+
* A single printable character typed on a selected cell starts an edit seeded
|
|
132
|
+
* with it — the spreadsheet reflex of "just start typing to replace".
|
|
133
|
+
*
|
|
134
|
+
* Modifier combinations are excluded so Cmd-C / Ctrl-R never get mistaken for
|
|
135
|
+
* typing, and only genuinely printable single characters qualify: `key` is the
|
|
136
|
+
* character itself for those, and a multi-character name ("ArrowUp", "F3") for
|
|
137
|
+
* everything else.
|
|
138
|
+
*/
|
|
139
|
+
declare function isTypingKey(e: {
|
|
140
|
+
key: string;
|
|
141
|
+
ctrlKey: boolean;
|
|
142
|
+
metaKey: boolean;
|
|
143
|
+
altKey: boolean;
|
|
144
|
+
}): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Translate a keystroke into a grid action. Called ONLY when a cell is selected
|
|
147
|
+
* and not being edited — an editor owns its own keys while it is open.
|
|
148
|
+
*/
|
|
149
|
+
declare function classifyGridKey(e: {
|
|
150
|
+
key: string;
|
|
151
|
+
shiftKey: boolean;
|
|
152
|
+
ctrlKey: boolean;
|
|
153
|
+
metaKey: boolean;
|
|
154
|
+
altKey: boolean;
|
|
155
|
+
}): GridKeyAction;
|
|
156
|
+
/**
|
|
157
|
+
* Editor kinds a SINGLE click may operate directly, without first entering edit
|
|
158
|
+
* mode. See THE CLICK LAW at the top of this file — this list is closed sets
|
|
159
|
+
* and two-state values only, where a mis-click is obvious and instantly undone.
|
|
160
|
+
*/
|
|
161
|
+
declare function directClickKinds(): readonly string[];
|
|
162
|
+
declare function isDirectClickEditor(editorKind: string | undefined): boolean;
|
|
163
|
+
/** Stable DOM key for a cell, so selection can scroll itself into view. */
|
|
164
|
+
declare function cellDomKey(address: CellAddress): string;
|
|
165
|
+
|
|
166
|
+
export { type CellAddress, type CellRange, type GridKeyAction, type GridMove, type RangeBounds, allRange, boundsContain, cellDomKey, cellsInRange, classifyGridKey, columnRange, directClickKinds, isDirectClickEditor, isSingleCellRange, isTypingKey, moveSelection, rangeBounds, rangeRows, rangeSize, rowRange, sameCell };
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
|
|
2
|
+
// src/data-table/grid-selection.ts
|
|
3
|
+
function rangeBounds(range, rowIds, fieldNames) {
|
|
4
|
+
const ra = rowIds.indexOf(range.anchor.rowId);
|
|
5
|
+
const rf = rowIds.indexOf(range.focus.rowId);
|
|
6
|
+
const ca = fieldNames.indexOf(range.anchor.fieldName);
|
|
7
|
+
const cf = fieldNames.indexOf(range.focus.fieldName);
|
|
8
|
+
if (ra === -1 || rf === -1 || ca === -1 || cf === -1) return null;
|
|
9
|
+
return {
|
|
10
|
+
r0: Math.min(ra, rf),
|
|
11
|
+
r1: Math.max(ra, rf),
|
|
12
|
+
c0: Math.min(ca, cf),
|
|
13
|
+
c1: Math.max(ca, cf)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function rangeSize(bounds) {
|
|
17
|
+
const rows = bounds.r1 - bounds.r0 + 1;
|
|
18
|
+
const cols = bounds.c1 - bounds.c0 + 1;
|
|
19
|
+
return { rows, cols, cells: rows * cols };
|
|
20
|
+
}
|
|
21
|
+
function boundsContain(bounds, rowIndex, colIndex) {
|
|
22
|
+
return rowIndex >= bounds.r0 && rowIndex <= bounds.r1 && colIndex >= bounds.c0 && colIndex <= bounds.c1;
|
|
23
|
+
}
|
|
24
|
+
function cellsInRange(range, rowIds, fieldNames) {
|
|
25
|
+
const b = rangeBounds(range, rowIds, fieldNames);
|
|
26
|
+
if (!b) return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let r = b.r0; r <= b.r1; r += 1) {
|
|
29
|
+
const rowId = rowIds[r];
|
|
30
|
+
if (rowId === void 0) continue;
|
|
31
|
+
for (let c = b.c0; c <= b.c1; c += 1) {
|
|
32
|
+
const fieldName = fieldNames[c];
|
|
33
|
+
if (fieldName === void 0) continue;
|
|
34
|
+
out.push({ rowId, fieldName });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function rangeRows(range, rowIds, fieldNames) {
|
|
40
|
+
const b = rangeBounds(range, rowIds, fieldNames);
|
|
41
|
+
if (!b) return [];
|
|
42
|
+
const rows = [];
|
|
43
|
+
for (let r = b.r0; r <= b.r1; r += 1) {
|
|
44
|
+
const rowId = rowIds[r];
|
|
45
|
+
if (rowId === void 0) continue;
|
|
46
|
+
const row = [];
|
|
47
|
+
for (let c = b.c0; c <= b.c1; c += 1) {
|
|
48
|
+
const fieldName = fieldNames[c];
|
|
49
|
+
if (fieldName === void 0) continue;
|
|
50
|
+
row.push({ rowId, fieldName });
|
|
51
|
+
}
|
|
52
|
+
rows.push(row);
|
|
53
|
+
}
|
|
54
|
+
return rows;
|
|
55
|
+
}
|
|
56
|
+
function isSingleCellRange(range) {
|
|
57
|
+
return sameCell(range.anchor, range.focus);
|
|
58
|
+
}
|
|
59
|
+
function rowRange(rowId, fieldNames) {
|
|
60
|
+
const firstField = fieldNames[0];
|
|
61
|
+
const lastField = fieldNames[fieldNames.length - 1];
|
|
62
|
+
if (firstField === void 0 || lastField === void 0) return null;
|
|
63
|
+
return {
|
|
64
|
+
anchor: { rowId, fieldName: firstField },
|
|
65
|
+
focus: { rowId, fieldName: lastField }
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function columnRange(fieldName, rowIds) {
|
|
69
|
+
const firstRow = rowIds[0];
|
|
70
|
+
const lastRow = rowIds[rowIds.length - 1];
|
|
71
|
+
if (firstRow === void 0 || lastRow === void 0) return null;
|
|
72
|
+
return {
|
|
73
|
+
anchor: { rowId: firstRow, fieldName },
|
|
74
|
+
focus: { rowId: lastRow, fieldName }
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function allRange(rowIds, fieldNames) {
|
|
78
|
+
const firstRow = rowIds[0];
|
|
79
|
+
const lastRow = rowIds[rowIds.length - 1];
|
|
80
|
+
const firstField = fieldNames[0];
|
|
81
|
+
const lastField = fieldNames[fieldNames.length - 1];
|
|
82
|
+
if (firstRow === void 0 || lastRow === void 0 || firstField === void 0 || lastField === void 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
anchor: { rowId: firstRow, fieldName: firstField },
|
|
87
|
+
focus: { rowId: lastRow, fieldName: lastField }
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function sameCell(a, b) {
|
|
91
|
+
if (!a || !b) return a === b;
|
|
92
|
+
return a.rowId === b.rowId && a.fieldName === b.fieldName;
|
|
93
|
+
}
|
|
94
|
+
function moveSelection(current, move, rowIds, fieldNames) {
|
|
95
|
+
if (rowIds.length === 0 || fieldNames.length === 0) return null;
|
|
96
|
+
const firstRowId = rowIds[0];
|
|
97
|
+
const firstFieldName = fieldNames[0];
|
|
98
|
+
if (firstRowId === void 0 || firstFieldName === void 0) return null;
|
|
99
|
+
const first = { rowId: firstRowId, fieldName: firstFieldName };
|
|
100
|
+
if (!current) return first;
|
|
101
|
+
let r = rowIds.indexOf(current.rowId);
|
|
102
|
+
let c = fieldNames.indexOf(current.fieldName);
|
|
103
|
+
if (r === -1 || c === -1) return first;
|
|
104
|
+
const lastRow = rowIds.length - 1;
|
|
105
|
+
const lastCol = fieldNames.length - 1;
|
|
106
|
+
const clamp = (n, max) => Math.min(Math.max(n, 0), max);
|
|
107
|
+
switch (move) {
|
|
108
|
+
case "up":
|
|
109
|
+
r = clamp(r - 1, lastRow);
|
|
110
|
+
break;
|
|
111
|
+
case "down":
|
|
112
|
+
r = clamp(r + 1, lastRow);
|
|
113
|
+
break;
|
|
114
|
+
case "left":
|
|
115
|
+
c = clamp(c - 1, lastCol);
|
|
116
|
+
break;
|
|
117
|
+
case "right":
|
|
118
|
+
c = clamp(c + 1, lastCol);
|
|
119
|
+
break;
|
|
120
|
+
case "rowStart":
|
|
121
|
+
c = 0;
|
|
122
|
+
break;
|
|
123
|
+
case "rowEnd":
|
|
124
|
+
c = lastCol;
|
|
125
|
+
break;
|
|
126
|
+
case "gridStart":
|
|
127
|
+
r = 0;
|
|
128
|
+
c = 0;
|
|
129
|
+
break;
|
|
130
|
+
case "gridEnd":
|
|
131
|
+
r = lastRow;
|
|
132
|
+
c = lastCol;
|
|
133
|
+
break;
|
|
134
|
+
case "nextCell":
|
|
135
|
+
if (c < lastCol) c += 1;
|
|
136
|
+
else if (r < lastRow) {
|
|
137
|
+
r += 1;
|
|
138
|
+
c = 0;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
case "prevCell":
|
|
142
|
+
if (c > 0) c -= 1;
|
|
143
|
+
else if (r > 0) {
|
|
144
|
+
r -= 1;
|
|
145
|
+
c = lastCol;
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const rowId = rowIds[r];
|
|
150
|
+
const fieldName = fieldNames[c];
|
|
151
|
+
if (rowId === void 0 || fieldName === void 0) return first;
|
|
152
|
+
return { rowId, fieldName };
|
|
153
|
+
}
|
|
154
|
+
function isTypingKey(e) {
|
|
155
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
156
|
+
return e.key.length === 1 && e.key !== " ";
|
|
157
|
+
}
|
|
158
|
+
function classifyGridKey(e) {
|
|
159
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
160
|
+
const arrowMove = {
|
|
161
|
+
ArrowUp: mod ? "gridStart" : "up",
|
|
162
|
+
ArrowDown: mod ? "gridEnd" : "down",
|
|
163
|
+
ArrowLeft: mod ? "rowStart" : "left",
|
|
164
|
+
ArrowRight: mod ? "rowEnd" : "right",
|
|
165
|
+
Home: mod ? "gridStart" : "rowStart",
|
|
166
|
+
End: mod ? "gridEnd" : "rowEnd"
|
|
167
|
+
};
|
|
168
|
+
const arrow = arrowMove[e.key];
|
|
169
|
+
if (arrow !== void 0) {
|
|
170
|
+
return { kind: e.shiftKey ? "extend" : "move", move: arrow };
|
|
171
|
+
}
|
|
172
|
+
switch (e.key) {
|
|
173
|
+
case "Tab":
|
|
174
|
+
return { kind: "move", move: e.shiftKey ? "prevCell" : "nextCell" };
|
|
175
|
+
case "Enter":
|
|
176
|
+
case "F2":
|
|
177
|
+
return { kind: "edit" };
|
|
178
|
+
case " ":
|
|
179
|
+
if (e.shiftKey) return { kind: "selectRow" };
|
|
180
|
+
if (mod) return { kind: "selectColumn" };
|
|
181
|
+
return { kind: "edit" };
|
|
182
|
+
case "a":
|
|
183
|
+
case "A":
|
|
184
|
+
if (mod) return { kind: "selectAll" };
|
|
185
|
+
break;
|
|
186
|
+
case "d":
|
|
187
|
+
case "D":
|
|
188
|
+
if (mod) return { kind: "fillDown" };
|
|
189
|
+
break;
|
|
190
|
+
case "Escape":
|
|
191
|
+
return { kind: "escape" };
|
|
192
|
+
case "Delete":
|
|
193
|
+
case "Backspace":
|
|
194
|
+
return { kind: "clearCell" };
|
|
195
|
+
case "c":
|
|
196
|
+
case "C":
|
|
197
|
+
if (mod) return { kind: "copy" };
|
|
198
|
+
break;
|
|
199
|
+
case "x":
|
|
200
|
+
case "X":
|
|
201
|
+
if (mod) return { kind: "cut" };
|
|
202
|
+
break;
|
|
203
|
+
case "v":
|
|
204
|
+
case "V":
|
|
205
|
+
if (mod) return { kind: "paste" };
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if (isTypingKey(e)) return { kind: "editSeeded", seed: e.key };
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
function directClickKinds() {
|
|
212
|
+
return ["checkbox", "rating", "select", "multiselect"];
|
|
213
|
+
}
|
|
214
|
+
function isDirectClickEditor(editorKind) {
|
|
215
|
+
return editorKind !== void 0 && directClickKinds().includes(editorKind);
|
|
216
|
+
}
|
|
217
|
+
function cellDomKey(address) {
|
|
218
|
+
return `${address.rowId}::${address.fieldName}`;
|
|
219
|
+
}
|
|
220
|
+
export {
|
|
221
|
+
allRange,
|
|
222
|
+
boundsContain,
|
|
223
|
+
cellDomKey,
|
|
224
|
+
cellsInRange,
|
|
225
|
+
classifyGridKey,
|
|
226
|
+
columnRange,
|
|
227
|
+
directClickKinds,
|
|
228
|
+
isDirectClickEditor,
|
|
229
|
+
isSingleCellRange,
|
|
230
|
+
isTypingKey,
|
|
231
|
+
moveSelection,
|
|
232
|
+
rangeBounds,
|
|
233
|
+
rangeRows,
|
|
234
|
+
rangeSize,
|
|
235
|
+
rowRange,
|
|
236
|
+
sameCell
|
|
237
|
+
};
|
|
238
|
+
//# sourceMappingURL=grid-selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/grid-selection.ts"],"sourcesContent":["/**\n * Grid selection model for user data tables.\n * @ai-matrx/design-system/data-table/grid-selection\n *\n * This moved here from the app so every table in the product shares one\n * range-selection model.\n *\n * THE THREE STATES. A spreadsheet grid has three, not two, and collapsing them\n * to two is what made this grid feel like a web page instead of a tool:\n *\n * 1. NOTHING SELECTED — the grid at rest.\n * 2. SELECTED — one cell is current. Nothing has changed. You can\n * look, copy, and move with the keyboard.\n * 3. EDITING — a value is being changed.\n *\n * State 2 is the one that was missing, and its absence is why every edit needed\n * a double-click and why arrow keys, Tab, copy and fill-down were all\n * impossible: none of them mean anything without a notion of \"the current cell\".\n *\n * 🚨 THE CLICK LAW — a single click may SELECT a cell, TOGGLE a two-state\n * value, or OPEN a chooser. It may NEVER drop the user into a free-text buffer.\n * Opening a menu is not a mutation and toggling a checkbox is instantly\n * reversible, but landing in a text buffer turns every attempt to select-and-\n * copy into an accidental edit. `directClickKinds()` below is the whole list of\n * what a single click may act on; adding a free-text editor to it is a defect.\n *\n * A CHOOSER OPENS ON THE SECOND CLICK. A choice cell's first click SELECTS it\n * (so Cmd-C, Cmd-X, the arrow keys and the right-click menu all have a current\n * cell); a click on the cell that is ALREADY selected opens the option list —\n * the Airtable gesture. Opening the chooser on the very first click moved\n * focus into its search box, and every keyboard shortcut then went to that box\n * instead of the grid: a choice column could not be copied at all (2026-09-14).\n * Checkboxes and ratings stay one-click: a mis-click on those is instantly\n * undone in place and steals no focus.\n *\n * CLIPBOARD KEYS. Cmd-C / Cmd-X / Cmd-V on a selected cell act on THAT CELL —\n * copy its value, cut it (copy + clear), paste over it. A paste carrying a\n * spreadsheet block (tabs / line breaks) lands as a block from the selected\n * cell downward and rightward (`grid-clipboard.ts`). None of these need an\n * editor open, which is the whole point: a normal table copies what you point\n * at, not only what you are typing into.\n *\n * Addresses are (rowId, fieldName), never (rowIndex, colIndex): the grid\n * reloads after every write, and realtime reorders rows underneath the user. An\n * index-based selection silently jumps to a DIFFERENT cell when that happens —\n * and the next keystroke then edits the wrong row.\n *\n * Pure module: no React, no DOM. Everything here is a function of the current\n * address and the row/field lists, which is what makes the navigation testable\n * without mounting a grid.\n */\n\nexport type CellAddress = { rowId: string; fieldName: string };\n\nexport type GridMove =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"nextCell\"\n | \"prevCell\"\n | \"rowStart\"\n | \"rowEnd\"\n | \"gridStart\"\n | \"gridEnd\";\n\n/** What a keystroke means when a cell is selected but NOT being edited. */\nexport type GridKeyAction =\n | { kind: \"move\"; move: GridMove }\n /** Shift+arrow: grow the range toward `move`, anchor fixed. */\n | { kind: \"extend\"; move: GridMove }\n | { kind: \"selectAll\" }\n | { kind: \"selectRow\" }\n | { kind: \"selectColumn\" }\n | { kind: \"fillDown\" }\n | { kind: \"edit\" }\n | { kind: \"editSeeded\"; seed: string }\n | { kind: \"clearCell\" }\n | { kind: \"copy\" }\n | { kind: \"cut\" }\n | { kind: \"paste\" }\n | { kind: \"escape\" }\n | null;\n\n// ─── Ranges ─────────────────────────────────────────────────────────────────\n//\n// A RANGE is an anchor (where the selection started — the cell with the ring)\n// and a focus (where it was extended to), both by ADDRESS. The rectangle they\n// span is resolved against the grid's current row and column order at the\n// moment it is needed, so a range survives a re-sort or a hidden column the\n// same way a single selection does: by naming cells, not positions.\n\nexport type CellRange = { anchor: CellAddress; focus: CellAddress };\n\n/** The rectangle a range spans, as inclusive indexes into the grid's order. */\nexport type RangeBounds = { r0: number; r1: number; c0: number; c1: number };\n\nexport function rangeBounds(\n range: CellRange,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): RangeBounds | null {\n const ra = rowIds.indexOf(range.anchor.rowId);\n const rf = rowIds.indexOf(range.focus.rowId);\n const ca = fieldNames.indexOf(range.anchor.fieldName);\n const cf = fieldNames.indexOf(range.focus.fieldName);\n if (ra === -1 || rf === -1 || ca === -1 || cf === -1) return null;\n return {\n r0: Math.min(ra, rf),\n r1: Math.max(ra, rf),\n c0: Math.min(ca, cf),\n c1: Math.max(ca, cf),\n };\n}\n\nexport function rangeSize(bounds: RangeBounds): {\n rows: number;\n cols: number;\n cells: number;\n} {\n const rows = bounds.r1 - bounds.r0 + 1;\n const cols = bounds.c1 - bounds.c0 + 1;\n return { rows, cols, cells: rows * cols };\n}\n\nexport function boundsContain(\n bounds: RangeBounds,\n rowIndex: number,\n colIndex: number,\n): boolean {\n return (\n rowIndex >= bounds.r0 &&\n rowIndex <= bounds.r1 &&\n colIndex >= bounds.c0 &&\n colIndex <= bounds.c1\n );\n}\n\n/** Every cell of the range in reading order (row by row, left to right). */\nexport function cellsInRange(\n range: CellRange,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellAddress[] {\n const b = rangeBounds(range, rowIds, fieldNames);\n if (!b) return [];\n const out: CellAddress[] = [];\n // `rangeBounds` derives every index from `indexOf`, so both lookups below are\n // in range; the guards make that explicit instead of asserting it.\n for (let r = b.r0; r <= b.r1; r += 1) {\n const rowId = rowIds[r];\n if (rowId === undefined) continue;\n for (let c = b.c0; c <= b.c1; c += 1) {\n const fieldName = fieldNames[c];\n if (fieldName === undefined) continue;\n out.push({ rowId, fieldName });\n }\n }\n return out;\n}\n\n/** The range as a 2-D grid of addresses — what a block copy serialises. */\nexport function rangeRows(\n range: CellRange,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellAddress[][] {\n const b = rangeBounds(range, rowIds, fieldNames);\n if (!b) return [];\n const rows: CellAddress[][] = [];\n // Same in-range guarantee as `cellsInRange`.\n for (let r = b.r0; r <= b.r1; r += 1) {\n const rowId = rowIds[r];\n if (rowId === undefined) continue;\n const row: CellAddress[] = [];\n for (let c = b.c0; c <= b.c1; c += 1) {\n const fieldName = fieldNames[c];\n if (fieldName === undefined) continue;\n row.push({ rowId, fieldName });\n }\n rows.push(row);\n }\n return rows;\n}\n\nexport function isSingleCellRange(range: CellRange): boolean {\n return sameCell(range.anchor, range.focus);\n}\n\n/** The whole row, first column to last — what clicking a row's edge selects. */\nexport function rowRange(\n rowId: string,\n fieldNames: readonly string[],\n): CellRange | null {\n const firstField = fieldNames[0];\n const lastField = fieldNames[fieldNames.length - 1];\n // A non-empty list always has both ends; the null here is the same \"no range\n // to make\" answer the empty-list case gives.\n if (firstField === undefined || lastField === undefined) return null;\n return {\n anchor: { rowId, fieldName: firstField },\n focus: { rowId, fieldName: lastField },\n };\n}\n\n/** The whole column across the page — what clicking a header's edge selects. */\nexport function columnRange(\n fieldName: string,\n rowIds: readonly string[],\n): CellRange | null {\n const firstRow = rowIds[0];\n const lastRow = rowIds[rowIds.length - 1];\n if (firstRow === undefined || lastRow === undefined) return null;\n return {\n anchor: { rowId: firstRow, fieldName },\n focus: { rowId: lastRow, fieldName },\n };\n}\n\n/** Every cell on the page. */\nexport function allRange(\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellRange | null {\n const firstRow = rowIds[0];\n const lastRow = rowIds[rowIds.length - 1];\n const firstField = fieldNames[0];\n const lastField = fieldNames[fieldNames.length - 1];\n if (\n firstRow === undefined ||\n lastRow === undefined ||\n firstField === undefined ||\n lastField === undefined\n ) {\n return null;\n }\n return {\n anchor: { rowId: firstRow, fieldName: firstField },\n focus: { rowId: lastRow, fieldName: lastField },\n };\n}\n\nexport function sameCell(\n a: CellAddress | null,\n b: CellAddress | null,\n): boolean {\n if (!a || !b) return a === b;\n return a.rowId === b.rowId && a.fieldName === b.fieldName;\n}\n\n/**\n * Where does `move` land from `current`?\n *\n * Returns null when the move is impossible (no selection, empty grid). Vertical\n * moves CLAMP at the edges — running into the top of the grid should park you\n * on the first row, not clear the selection, because a selection that vanishes\n * when you overshoot loses your place. Horizontal Tab moves WRAP to the next or\n * previous row, which is what makes Tab a usable data-entry gesture.\n */\nexport function moveSelection(\n current: CellAddress | null,\n move: GridMove,\n rowIds: readonly string[],\n fieldNames: readonly string[],\n): CellAddress | null {\n if (rowIds.length === 0 || fieldNames.length === 0) return null;\n\n const firstRowId = rowIds[0];\n const firstFieldName = fieldNames[0];\n // Both lists are non-empty by the check above; `null` here is the same \"no\n // move is possible\" answer the empty-grid case gives.\n if (firstRowId === undefined || firstFieldName === undefined) return null;\n const first: CellAddress = { rowId: firstRowId, fieldName: firstFieldName };\n if (!current) return first;\n\n let r = rowIds.indexOf(current.rowId);\n let c = fieldNames.indexOf(current.fieldName);\n // The selected row or column disappeared (deleted, filtered out, re-sorted\n // off the page). Fall back to the start rather than computing from -1, which\n // would quietly resolve to the last element.\n if (r === -1 || c === -1) return first;\n\n const lastRow = rowIds.length - 1;\n const lastCol = fieldNames.length - 1;\n const clamp = (n: number, max: number) => Math.min(Math.max(n, 0), max);\n\n switch (move) {\n case \"up\":\n r = clamp(r - 1, lastRow);\n break;\n case \"down\":\n r = clamp(r + 1, lastRow);\n break;\n case \"left\":\n c = clamp(c - 1, lastCol);\n break;\n case \"right\":\n c = clamp(c + 1, lastCol);\n break;\n case \"rowStart\":\n c = 0;\n break;\n case \"rowEnd\":\n c = lastCol;\n break;\n case \"gridStart\":\n r = 0;\n c = 0;\n break;\n case \"gridEnd\":\n r = lastRow;\n c = lastCol;\n break;\n case \"nextCell\":\n if (c < lastCol) c += 1;\n else if (r < lastRow) {\n r += 1;\n c = 0;\n }\n break;\n case \"prevCell\":\n if (c > 0) c -= 1;\n else if (r > 0) {\n r -= 1;\n c = lastCol;\n }\n break;\n }\n\n // Every branch above clamps into range or leaves r/c at their `indexOf`\n // values, so both reads land; falling back to `first` matches what this\n // function already does when an index cannot be resolved.\n const rowId = rowIds[r];\n const fieldName = fieldNames[c];\n if (rowId === undefined || fieldName === undefined) return first;\n return { rowId, fieldName };\n}\n\n/**\n * A single printable character typed on a selected cell starts an edit seeded\n * with it — the spreadsheet reflex of \"just start typing to replace\".\n *\n * Modifier combinations are excluded so Cmd-C / Ctrl-R never get mistaken for\n * typing, and only genuinely printable single characters qualify: `key` is the\n * character itself for those, and a multi-character name (\"ArrowUp\", \"F3\") for\n * everything else.\n */\nexport function isTypingKey(e: {\n key: string;\n ctrlKey: boolean;\n metaKey: boolean;\n altKey: boolean;\n}): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey) return false;\n return e.key.length === 1 && e.key !== \" \";\n}\n\n/**\n * Translate a keystroke into a grid action. Called ONLY when a cell is selected\n * and not being edited — an editor owns its own keys while it is open.\n */\nexport function classifyGridKey(e: {\n key: string;\n shiftKey: boolean;\n ctrlKey: boolean;\n metaKey: boolean;\n altKey: boolean;\n}): GridKeyAction {\n const mod = e.ctrlKey || e.metaKey;\n\n // Shift+arrow GROWS the selection instead of moving it — the spreadsheet\n // gesture for \"these cells\". Shift+Tab stays a move (previous cell).\n const arrowMove: Record<string, GridMove> = {\n ArrowUp: mod ? \"gridStart\" : \"up\",\n ArrowDown: mod ? \"gridEnd\" : \"down\",\n ArrowLeft: mod ? \"rowStart\" : \"left\",\n ArrowRight: mod ? \"rowEnd\" : \"right\",\n Home: mod ? \"gridStart\" : \"rowStart\",\n End: mod ? \"gridEnd\" : \"rowEnd\",\n };\n // Read the entry rather than testing `in`: `in` also answers true for\n // inherited Object.prototype names (\"constructor\", \"toString\"), which used to\n // produce a move action carrying an undefined move.\n const arrow = arrowMove[e.key];\n if (arrow !== undefined) {\n return { kind: e.shiftKey ? \"extend\" : \"move\", move: arrow };\n }\n\n switch (e.key) {\n case \"Tab\":\n return { kind: \"move\", move: e.shiftKey ? \"prevCell\" : \"nextCell\" };\n case \"Enter\":\n case \"F2\":\n return { kind: \"edit\" };\n case \" \":\n // Shift+Space selects the row and Ctrl/Cmd+Space the column (Excel);\n // plain Space opens the editor without seeding it, so a boolean or\n // choice cell can be operated entirely from the keyboard.\n if (e.shiftKey) return { kind: \"selectRow\" };\n if (mod) return { kind: \"selectColumn\" };\n return { kind: \"edit\" };\n case \"a\":\n case \"A\":\n if (mod) return { kind: \"selectAll\" };\n break;\n case \"d\":\n case \"D\":\n if (mod) return { kind: \"fillDown\" };\n break;\n case \"Escape\":\n return { kind: \"escape\" };\n case \"Delete\":\n case \"Backspace\":\n return { kind: \"clearCell\" };\n case \"c\":\n case \"C\":\n if (mod) return { kind: \"copy\" };\n break;\n case \"x\":\n case \"X\":\n if (mod) return { kind: \"cut\" };\n break;\n case \"v\":\n case \"V\":\n if (mod) return { kind: \"paste\" };\n break;\n }\n\n if (isTypingKey(e)) return { kind: \"editSeeded\", seed: e.key };\n return null;\n}\n\n/**\n * Editor kinds a SINGLE click may operate directly, without first entering edit\n * mode. See THE CLICK LAW at the top of this file — this list is closed sets\n * and two-state values only, where a mis-click is obvious and instantly undone.\n */\nexport function directClickKinds(): readonly string[] {\n return [\"checkbox\", \"rating\", \"select\", \"multiselect\"] as const;\n}\n\nexport function isDirectClickEditor(editorKind: string | undefined): boolean {\n return editorKind !== undefined && directClickKinds().includes(editorKind);\n}\n\n/** Stable DOM key for a cell, so selection can scroll itself into view. */\nexport function cellDomKey(address: CellAddress): string {\n return `${address.rowId}::${address.fieldName}`;\n}\n"],"mappings":"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","names":[]}
|