@keenmate/web-grid 1.0.0-rc15 → 1.0.1

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.
@@ -0,0 +1,214 @@
1
+ ROW LOCKING
2
+ ===========
3
+ @keenmate/web-grid - Optimistic row locking with external lock management.
4
+
5
+
6
+ OVERVIEW
7
+ --------
8
+ Row locking prevents editing of rows that are locked by other users or
9
+ processes. Three lock sources exist: property-based, callback-based, and
10
+ external API. Locks are configured via the rowLocking property on the grid.
11
+ Row identification (idValueMember or idValueCallback) is required for
12
+ external lock methods.
13
+
14
+
15
+ CONFIGURATION
16
+ -------------
17
+ grid.rowLocking = {
18
+ lockedMember: 'isLocked',
19
+ lockInfoMember: 'lockInfo',
20
+ lockedEditBehavior: 'block',
21
+ lockTooltipCallback: (lockInfo, row) => {
22
+ return 'Locked by ' + lockInfo.lockedBy
23
+ }
24
+ }
25
+
26
+
27
+ LOCK SOURCE 1: PROPERTY-BASED
28
+ ------------------------------
29
+ lockedMember (keyof T)
30
+ Property name on the row object containing a boolean lock state.
31
+
32
+ grid.rowLocking = { lockedMember: 'isLocked' }
33
+ // Row: { id: 1, name: 'Alice', isLocked: true }
34
+
35
+ lockInfoMember (keyof T)
36
+ Property name on the row object containing a RowLockInfo object.
37
+
38
+ grid.rowLocking = { lockInfoMember: 'lockInfo' }
39
+ // Row: { id: 1, name: 'Alice', lockInfo: { isLocked: true, lockedBy: 'Bob' } }
40
+
41
+
42
+ LOCK SOURCE 2: CALLBACK-BASED
43
+ ------------------------------
44
+ isLockedCallback (row: T, rowIndex: number) => boolean
45
+ Callback that determines if a row is locked.
46
+
47
+ getLockInfoCallback (row: T, rowIndex: number) => RowLockInfo | null
48
+ Callback returning detailed lock information.
49
+
50
+ grid.rowLocking = {
51
+ isLockedCallback: (row) => row.status === 'in-review',
52
+ getLockInfoCallback: (row) => ({
53
+ isLocked: row.status === 'in-review',
54
+ lockedBy: row.reviewer,
55
+ reason: 'Under review'
56
+ })
57
+ }
58
+
59
+
60
+ LOCK SOURCE 3: EXTERNAL API
61
+ ----------------------------
62
+ For real-time lock management (e.g., WebSocket-driven collaborative editing).
63
+ Requires idValueMember or idValueCallback to be set.
64
+
65
+ lockRowById(id, lockerInfo?)
66
+ Lock a row by its ID. Returns boolean (true if row found).
67
+ lockerInfo is an optional RowLockInfo object.
68
+
69
+ grid.lockRowById(42, {
70
+ isLocked: true,
71
+ lockedBy: 'Jane',
72
+ lockedAt: new Date()
73
+ })
74
+
75
+ unlockRowById(id)
76
+ Unlock an externally locked row. Returns boolean.
77
+
78
+ grid.unlockRowById(42)
79
+
80
+ getExternalLocks()
81
+ Returns Map<unknown, RowLockInfo> of all external locks.
82
+
83
+ clearExternalLocks()
84
+ Remove all external locks at once.
85
+
86
+
87
+ ROWLOCKINFO TYPE
88
+ ----------------
89
+ {
90
+ isLocked: boolean
91
+ lockedBy?: string Who locked (user name or ID)
92
+ lockedAt?: Date | string When locked
93
+ reason?: string Why locked
94
+ [key: string]: unknown Extra properties allowed
95
+ }
96
+
97
+
98
+ LOCKED EDIT BEHAVIOR
99
+ --------------------
100
+ lockedEditBehavior on RowLockingOptions<T>
101
+ Controls what happens when a user tries to edit a locked row.
102
+
103
+ 'block' (default)
104
+ Editing is completely blocked. Cells show cursor: not-allowed.
105
+
106
+ 'allow'
107
+ Editing is allowed despite the lock. Only visual indicators are shown.
108
+
109
+ 'callback'
110
+ Consumer decides per-row via canEditLockedCallback.
111
+
112
+
113
+ canEditLockedCallback (row: T, lockInfo: RowLockInfo) => boolean
114
+ Only used when lockedEditBehavior is 'callback'. Return true to allow
115
+ editing the specific locked row, false to block it.
116
+
117
+ grid.rowLocking = {
118
+ lockedEditBehavior: 'callback',
119
+ canEditLockedCallback: (row, lockInfo) => {
120
+ return lockInfo.lockedBy === currentUser
121
+ }
122
+ }
123
+
124
+
125
+ LOCK TOOLTIP
126
+ ------------
127
+ lockTooltipCallback (lockInfo: RowLockInfo, row: T) => string | null
128
+ Returns HTML string for the tooltip shown when hovering the lock icon.
129
+ Return null for no tooltip.
130
+
131
+ grid.rowLocking = {
132
+ lockTooltipCallback: (lockInfo) => {
133
+ return '<strong>Locked by:</strong> ' + lockInfo.lockedBy +
134
+ '<br><strong>Since:</strong> ' + lockInfo.lockedAt
135
+ }
136
+ }
137
+
138
+
139
+ VISUAL INDICATORS
140
+ -----------------
141
+ Locked rows receive the following visual treatment:
142
+ - Row gets wg__row--locked CSS class
143
+ - Muted styling: cells have reduced opacity (--wg-row-locked-opacity: 0.7)
144
+ - Background: --wg-row-locked-bg (defaults to disabled/surface-2)
145
+ - Editable cells show cursor: not-allowed (when behavior is 'block')
146
+ - Hover effects on editable cells are suppressed
147
+ - Lock icon appears in the row number column (wg__row-number--locked class)
148
+ - Lock icon has cursor: help and full opacity (overrides row opacity)
149
+
150
+ CSS variables:
151
+ --wg-row-locked-bg Default: var(--base-disabled-bg, var(--wg-surface-2))
152
+ --wg-row-locked-opacity Default: 0.7
153
+
154
+
155
+ ONROWLOCKCHANGE EVENT
156
+ ---------------------
157
+ Fires when a row's lock state changes.
158
+
159
+ grid.onrowlockchange = (detail) => {
160
+ console.log('Row', detail.rowId, 'lock changed')
161
+ console.log('Source:', detail.source) // 'property', 'callback', or 'external'
162
+ }
163
+
164
+ RowLockChangeDetail<T>:
165
+ rowId: unknown Row identifier
166
+ row: T | null Row data (null if row not found)
167
+ rowIndex: number Row index
168
+ lockInfo: RowLockInfo | null Current lock info (null if unlocked)
169
+ source: 'property' | 'callback' | 'external'
170
+
171
+
172
+ QUERY METHODS
173
+ -------------
174
+ isRowLocked(rowOrId)
175
+ Returns boolean. Accepts a row object or a row ID.
176
+
177
+ getRowLockInfo(rowOrId)
178
+ Returns RowLockInfo | null. Accepts a row object or a row ID.
179
+
180
+
181
+ WEBSOCKET INTEGRATION EXAMPLE
182
+ ------------------------------
183
+ grid.idValueMember = 'id'
184
+ grid.rowLocking = {
185
+ lockedEditBehavior: 'block',
186
+ lockTooltipCallback: (info) => 'Locked by ' + info.lockedBy
187
+ }
188
+
189
+ websocket.onmessage = (event) => {
190
+ const msg = JSON.parse(event.data)
191
+ if (msg.type === 'row-locked') {
192
+ grid.lockRowById(msg.rowId, {
193
+ isLocked: true,
194
+ lockedBy: msg.user,
195
+ lockedAt: new Date()
196
+ })
197
+ }
198
+ if (msg.type === 'row-unlocked') {
199
+ grid.unlockRowById(msg.rowId)
200
+ }
201
+ }
202
+
203
+
204
+ REQUIREMENTS
205
+ ------------
206
+ External lock methods (lockRowById, unlockRowById, etc.) require row
207
+ identification to be configured:
208
+
209
+ grid.idValueMember = 'id'
210
+ // or
211
+ grid.idValueCallback = (row) => row.id
212
+
213
+ Without this, the grid cannot find rows by ID and the lock methods will
214
+ return false.
@@ -0,0 +1,403 @@
1
+ SELECTION FEATURES IN @keenmate/web-grid
2
+ =========================================
3
+ The grid supports four distinct selection types: row selection, cell range
4
+ selection, column selection, and row focus. Each is independent -- selecting
5
+ in one mode clears the others (row, cell range, column are mutually exclusive).
6
+ Row focus is a separate concept that tracks which row the user last clicked.
7
+
8
+
9
+ ROW SELECTION
10
+ -------------
11
+ Rows are selected by clicking row number cells (requires isRowNumbersVisible).
12
+ Selected row indices are tracked in a Set internally. The selectedRows property
13
+ returns them as a sorted array.
14
+
15
+ Prerequisite:
16
+ grid.isRowNumbersVisible = true
17
+
18
+ Interaction patterns:
19
+ Click row number - Select single row (replaces previous selection)
20
+ Ctrl+Click row number - Toggle row in/out of selection (non-contiguous)
21
+ Shift+Click row number - Select range from last selected to clicked row
22
+ Click+Drag row numbers - Drag to select contiguous range (5px threshold)
23
+ Escape - Clear row selection
24
+ Click on data cell - Clears row selection (only row numbers select)
25
+ Click row number header - Select all cells (selectAll, creates cell range)
26
+
27
+ Properties (on grid element):
28
+ selectedRows - number[] (read-only, sorted ascending)
29
+
30
+ Methods:
31
+ selectRow(rowIndex, mode?)
32
+ mode: 'replace' (default) - clear previous, select this row
33
+ mode: 'toggle' - add/remove this row from selection
34
+ mode: 'range' - select from last selected row to this row
35
+
36
+ selectRowRange(fromIndex, toIndex)
37
+ Selects all rows between fromIndex and toIndex (inclusive, order agnostic).
38
+
39
+ clearSelection()
40
+ Clears all selected rows.
41
+
42
+ isRowSelected(rowIndex)
43
+ Returns boolean.
44
+
45
+ getSelectedRowsData()
46
+ Returns T[] - the actual data objects for selected rows.
47
+
48
+ copySelectedRowsToClipboard()
49
+ Returns Promise<boolean>. Copies selected rows as TSV (tab-separated).
50
+ Respects shouldCopyWithHeaders. Uses raw values (no beforeCopyCallback).
51
+
52
+ Example:
53
+ grid.isRowNumbersVisible = true
54
+ grid.selectRow(0, 'replace')
55
+ grid.selectRow(2, 'toggle')
56
+ grid.selectRow(5, 'range')
57
+ console.log(grid.selectedRows) // [0, 2, 3, 4, 5]
58
+ console.log(grid.getSelectedRowsData()) // array of row objects
59
+
60
+
61
+ CELL RANGE SELECTION
62
+ --------------------
63
+ Rectangular cell ranges are selected by click+drag or shift+click on data cells.
64
+ The behavior depends on cellSelectionMode.
65
+
66
+ Property:
67
+ cellSelectionMode - CellSelectionMode
68
+ 'disabled' - No cell range selection
69
+ 'click' - Click+drag on cells to select range (default, used in excel/read-only modes)
70
+ 'shift' - Shift+click to select range; plain click edits (used in input-matrix mode)
71
+
72
+ selectedCellRange - CellRange | null (read-only)
73
+ shouldCopyWithHeaders - boolean (default false). Include column headers in clipboard copy.
74
+
75
+ CellRange type:
76
+ {
77
+ startRowIndex: number
78
+ startColIndex: number
79
+ endRowIndex: number
80
+ endColIndex: number
81
+ startField: string
82
+ endField: string
83
+ }
84
+
85
+ Interaction patterns (cellSelectionMode = 'click'):
86
+ Click+drag on cells - Select rectangular range (5px drag threshold)
87
+ Escape during drag - Cancel selection, restore focus to start cell
88
+ Return to start cell - Cancel selection, restore focus
89
+ Escape after selection - Clear cell selection
90
+ Ctrl+C - Copy selected cells to clipboard
91
+
92
+ Interaction patterns (cellSelectionMode = 'shift'):
93
+ Shift+Click - Select range from last clicked cell to current
94
+ Escape - Clear cell selection
95
+
96
+ Methods:
97
+ selectCellRange(range)
98
+ Programmatically select a CellRange. Clears row/column selection.
99
+ Fires oncellselectionchange callback.
100
+
101
+ clearCellSelection()
102
+ Clears cell range selection. Fires oncellselectionchange with null range.
103
+
104
+ getSelectedCells()
105
+ Returns Array<{ row: T, rowIndex: number, colIndex: number, field: string, value: unknown }>
106
+ Iterates through all cells in the rectangular range.
107
+
108
+ copyCellSelectionToClipboard()
109
+ Returns Promise<boolean>. Copies cells as TSV. Respects shouldCopyWithHeaders.
110
+
111
+ selectAll()
112
+ Selects all cells (entire visible data range as one CellRange).
113
+ Triggered by clicking the row number header cell (#).
114
+
115
+ Callback:
116
+ oncellselectionchange - (detail: { range: CellRange | null, cellCount: number }) => void
117
+ Fires when cell selection changes (created or cleared).
118
+
119
+ Example:
120
+ grid.cellSelectionMode = 'click'
121
+
122
+ grid.selectCellRange({
123
+ startRowIndex: 0, startColIndex: 1,
124
+ endRowIndex: 3, endColIndex: 4,
125
+ startField: 'name', endField: 'status'
126
+ })
127
+
128
+ grid.oncellselectionchange = (detail) => {
129
+ console.log('Selected', detail.cellCount, 'cells')
130
+ }
131
+
132
+ const cells = grid.getSelectedCells()
133
+ // [{ row: {...}, rowIndex: 0, colIndex: 1, field: 'name', value: 'Alice' }, ...]
134
+
135
+ Grid mode defaults for cellSelectionMode:
136
+ mode: 'read-only' -> cellSelectionMode: 'click'
137
+ mode: 'excel' -> cellSelectionMode: 'click'
138
+ mode: 'input-matrix' -> cellSelectionMode: 'shift'
139
+
140
+
141
+ COLUMN SELECTION
142
+ ----------------
143
+ Columns are selected by clicking column headers. Supports multi-column selection
144
+ with Ctrl and Shift modifiers, plus drag-to-select.
145
+
146
+ When isColumnReorderAllowed is true:
147
+ Plain click on header - Starts column reorder drag (NOT selection)
148
+ Ctrl+Click on header - Toggle column selection
149
+ Shift+Click on header - Range selection from last to current
150
+ Shift+Drag on header - Drag to select range of columns
151
+
152
+ When isColumnReorderAllowed is false:
153
+ Click on header - Select single column (replace)
154
+ Ctrl+Click on header - Toggle column selection
155
+ Shift+Click on header - Range selection from last to current
156
+ Click+Drag on header - Drag to select range of columns
157
+
158
+ Selecting columns clears row selection and cell range selection.
159
+
160
+ Properties (on grid instance):
161
+ selectedColumns - number[] (read-only, sorted ascending, visual indices)
162
+
163
+ Methods:
164
+ selectColumn(colIndex, mode?)
165
+ mode: 'replace' (default), 'toggle', 'range'
166
+
167
+ selectColumnRange(fromIndex, toIndex)
168
+ Selects all columns between fromIndex and toIndex (inclusive).
169
+
170
+ clearColumnSelection()
171
+ Clears all selected columns.
172
+
173
+ isColumnSelected(colIndex)
174
+ Returns boolean.
175
+
176
+ copySelectedColumnsToClipboard()
177
+ Returns Promise<boolean>. Copies all rows for selected columns as TSV.
178
+ Respects shouldCopyWithHeaders.
179
+
180
+ Visual feedback:
181
+ Selected column headers get the CSS class: wg__header--selected
182
+ Selected column cells get the CSS class: wg__cell--column-selected
183
+ A border overlay is drawn around contiguous column segments.
184
+
185
+ Example:
186
+ grid.isColumnReorderAllowed = false
187
+ // User clicks "Name" header -> column selected
188
+ // User Shift+clicks "Email" header -> range from Name to Email selected
189
+ console.log(grid.selectedColumns) // [1, 2, 3] (visual indices)
190
+
191
+
192
+ ROW FOCUS
193
+ ---------
194
+ Row focus tracks which row the user most recently interacted with via a data
195
+ cell click or programmatic focus. It is separate from row selection.
196
+
197
+ Key distinction: clicking a ROW NUMBER selects the row but does NOT focus it.
198
+ Clicking a DATA CELL focuses the row (and clears row selection).
199
+
200
+ Property:
201
+ focusedRowIndex - number | null (readable/writable)
202
+ Set to a number to programmatically focus a row.
203
+ Set to null to clear focus.
204
+
205
+ Callback:
206
+ onrowfocus - (detail: RowFocusDetail<T>) => void
207
+ Fires when a different row is focused.
208
+
209
+ RowFocusDetail type:
210
+ {
211
+ rowIndex: number
212
+ row: T
213
+ previousRowIndex: number | null
214
+ }
215
+
216
+ Master/detail pattern:
217
+ Use onrowfocus to update a detail panel when the user clicks different rows.
218
+
219
+ grid.onrowfocus = (detail) => {
220
+ detailPanel.innerHTML = renderDetail(detail.row)
221
+ console.log('Moved from row', detail.previousRowIndex, 'to', detail.rowIndex)
222
+ }
223
+
224
+ Behavior:
225
+ - Click on data cell -> sets focusedRowIndex, fires onrowfocus
226
+ - Click on row number -> selects row but does NOT change focus
227
+ - Click outside grid -> clears focus (focusedRowIndex becomes null)
228
+ - Starting cell range drag -> clears focus
229
+ - Programmatic: grid.focusedRowIndex = 3 (focuses row 3, fires onrowfocus)
230
+ - Programmatic: grid.focusedRowIndex = null (clears focus)
231
+
232
+ The focused row gets CSS class: wg__row--focused
233
+
234
+
235
+ CLIPBOARD
236
+ ---------
237
+ All clipboard operations produce TSV (tab-separated values) format, which is
238
+ compatible with Excel, Google Sheets, and other spreadsheet applications.
239
+
240
+ Format:
241
+ Columns separated by tab (\t), rows separated by newline (\n).
242
+ If shouldCopyWithHeaders is true, the first line contains column titles.
243
+
244
+ Built-in Ctrl+C behavior:
245
+ When a selection exists and the container is focused, Ctrl+C copies:
246
+ 1. Cell range selection -> copyCellSelectionToClipboard()
247
+ 2. Column selection -> copySelectedColumnsToClipboard()
248
+ 3. Row selection -> copySelectedRowsToClipboard()
249
+ 4. Single focused cell -> copies that cell's value
250
+
251
+ Priority order: cell range > column > row > single cell.
252
+
253
+ shouldCopyWithHeaders property:
254
+ grid.shouldCopyWithHeaders = true
255
+
256
+ When true, the first TSV row is column titles. Applies to all copy methods:
257
+ copySelectedRowsToClipboard(), copyCellSelectionToClipboard(),
258
+ copySelectedColumnsToClipboard().
259
+
260
+ Example output (shouldCopyWithHeaders = true):
261
+ Name\tAge\tEmail
262
+ Alice\t28\talice@example.com
263
+ Bob\t34\tbob@example.com
264
+
265
+ Example output (shouldCopyWithHeaders = false):
266
+ Alice\t28\talice@example.com
267
+ Bob\t34\tbob@example.com
268
+
269
+ Per-column beforeCopyCallback:
270
+ Transforms a cell value before it is written to the clipboard.
271
+ Currently applied only when copying a SINGLE focused cell via Ctrl+C.
272
+ NOT applied by the bulk copy methods (copySelectedRowsToClipboard, etc.).
273
+
274
+ column definition:
275
+ { field: 'price', beforeCopyCallback: (value, row) => '$' + value }
276
+
277
+ Per-column beforePasteCallback:
278
+ Processes a pasted value before applying it to a cell.
279
+
280
+ column definition:
281
+ { field: 'price', beforePasteCallback: (value, row) => parseFloat(value) }
282
+
283
+
284
+ RANGE SHORTCUTS
285
+ ---------------
286
+ Range shortcuts are keyboard shortcuts that operate on multiple selected rows
287
+ or a cell range. They work with both row selection and cell range selection.
288
+
289
+ Property:
290
+ rangeShortcuts - RangeShortcut<T>[]
291
+
292
+ RangeShortcut type:
293
+ {
294
+ key: string // e.g., "Delete", "Ctrl+E", "Shift+F2"
295
+ id: string // Unique identifier
296
+ label: string // Display label for shortcuts help overlay
297
+ action: (ctx: RangeShortcutContext<T>) => void | Promise<void>
298
+ disabled?: boolean | ((ctx: RangeShortcutContext<T>) => boolean)
299
+ }
300
+
301
+ RangeShortcutContext type:
302
+ {
303
+ rows: T[] // Selected rows data (for row selection)
304
+ rowIndices: number[] // Selected row indices (sorted ascending)
305
+ cellRange?: CellRange // Present when cell range is selected
306
+ cells?: Array<{ // Present when cell range is selected
307
+ row: T
308
+ rowIndex: number
309
+ colIndex: number
310
+ field: string
311
+ value: unknown
312
+ }>
313
+ }
314
+
315
+ When triggered from row selection:
316
+ ctx.rows and ctx.rowIndices are populated.
317
+ ctx.cellRange and ctx.cells are undefined.
318
+
319
+ When triggered from cell range selection:
320
+ ctx.cellRange and ctx.cells are populated.
321
+ ctx.rows is empty array, ctx.rowIndices is empty array.
322
+
323
+ Range shortcuts are checked in TWO places:
324
+ 1. Container keydown (when row/column/cell selection is active and container
325
+ is focused -- i.e., no cell is focused in navigate mode)
326
+ 2. Cell keydown (when rows are selected while a cell has focus)
327
+
328
+ Built-in keyboard shortcuts (always active, no configuration needed):
329
+ Escape - Clear selection (cell range > column > row priority)
330
+ Ctrl+C - Copy selection to clipboard
331
+
332
+ Example:
333
+ grid.rangeShortcuts = [
334
+ {
335
+ key: 'Delete',
336
+ id: 'delete-rows',
337
+ label: 'Delete selected rows',
338
+ action: (ctx) => {
339
+ // Remove selected rows from data
340
+ grid.items = grid.items.filter((_, i) => !ctx.rowIndices.includes(i))
341
+ }
342
+ },
343
+ {
344
+ key: 'Ctrl+E',
345
+ id: 'export-selection',
346
+ label: 'Export selection',
347
+ action: (ctx) => {
348
+ if (ctx.cellRange) {
349
+ // Export cell range
350
+ const values = ctx.cells.map(c => c.value)
351
+ exportData(values)
352
+ } else {
353
+ // Export selected rows
354
+ exportData(ctx.rows)
355
+ }
356
+ },
357
+ disabled: (ctx) => ctx.rows.length === 0 && !ctx.cellRange
358
+ }
359
+ ]
360
+
361
+ To show a help overlay listing shortcuts:
362
+ grid.isShortcutsHelpVisible = true
363
+ grid.shortcutsHelpPosition = 'top-right' // or 'top-left'
364
+
365
+
366
+ SELECTION MUTUAL EXCLUSIVITY
367
+ -----------------------------
368
+ Row selection, cell range selection, and column selection are mutually exclusive.
369
+ Activating one clears the others automatically:
370
+
371
+ selectRow() -> clears cell range and column selection
372
+ selectCellRange() -> clears row and column selection
373
+ selectColumn() -> clears row and cell range selection
374
+
375
+ Row focus (focusedRowIndex) is independent and can coexist with row selection,
376
+ but is cleared when starting a cell range drag.
377
+
378
+ The selectAll() method creates a cell range (not row selection), spanning all
379
+ visible rows and columns. It is triggered by clicking the row number header (#).
380
+
381
+
382
+ VISUAL FEEDBACK
383
+ ---------------
384
+ CSS classes applied during selection:
385
+
386
+ Row selection:
387
+ .wg__row--selected - Applied to selected <tr> elements
388
+ .wg__row-selection-border - Overlay border around contiguous segments
389
+ .wg--selecting - On container during row drag selection
390
+
391
+ Cell range selection:
392
+ .wg__cell--in-range - Applied to cells within the range
393
+ .wg__cell-range-border - Overlay border around the range
394
+ .wg--selecting-cells - On container during cell drag selection
395
+
396
+ Column selection:
397
+ .wg__header--selected - Applied to selected column headers
398
+ .wg__cell--column-selected - Applied to cells in selected columns
399
+ .wg__column-selection-border - Overlay border around contiguous segments
400
+ .wg--selecting-columns - On container during column drag selection
401
+
402
+ Row focus:
403
+ .wg__row--focused - Applied to the focused row