@iyulab/flex-table 0.40.0 → 0.40.2

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.
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-CDEu8YQE.js";
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-CbIsTvod.js";
2
2
  export { o as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, r as flexTableLocale, a as renderCell };
@@ -0,0 +1 @@
1
+ export {};
package/dist/react.d.ts CHANGED
@@ -27,6 +27,13 @@ declare const FlexTableReactBase: import("@lit/react").ReactWebComponent<FlexTab
27
27
  onBatchUpdate: EventName<CustomEvent>;
28
28
  onContextMenu: EventName<CustomEvent>;
29
29
  onFilterError: EventName<CustomEvent>;
30
+ onRowReorder: EventName<CustomEvent>;
31
+ onColumnVisibilityChange: EventName<CustomEvent>;
32
+ onCommentChange: EventName<CustomEvent>;
33
+ onDataImport: EventName<CustomEvent>;
34
+ onFillHandleApply: EventName<CustomEvent>;
35
+ onFindReplace: EventName<CustomEvent>;
36
+ onHeaderContextMenu: EventName<CustomEvent>;
30
37
  }>;
31
38
  type BaseProps = React.ComponentProps<typeof FlexTableReactBase>;
32
39
  /**
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-CDEu8YQE.js";
1
+ import { t as e } from "./flex-table-CbIsTvod.js";
2
2
  import { i as t, t as n } from "./query-Dw4iO0ir.js";
3
3
  import { t as r } from "./view-DiLr-TdY.js";
4
4
  import i, { useCallback as a, useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
@@ -165,7 +165,14 @@ var p = u({
165
165
  onValidationError: "validation-error",
166
166
  onBatchUpdate: "batch-update",
167
167
  onContextMenu: "context-menu",
168
- onFilterError: "filter-error"
168
+ onFilterError: "filter-error",
169
+ onRowReorder: "row-reorder",
170
+ onColumnVisibilityChange: "column-visibility-change",
171
+ onCommentChange: "comment-change",
172
+ onDataImport: "data-import",
173
+ onFillHandleApply: "fill-handle-apply",
174
+ onFindReplace: "find-replace",
175
+ onHeaderContextMenu: "header-context-menu"
169
176
  }
170
177
  });
171
178
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.40.0",
3
+ "version": "0.40.2",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "files": [
35
35
  "dist",
36
+ "skills",
36
37
  "package.json",
37
38
  "README.md",
38
39
  "CHANGELOG.md",
@@ -71,7 +72,7 @@
71
72
  "odata-query": "^8.1.0"
72
73
  },
73
74
  "peerDependencies": {
74
- "@iyulab/components": ">=1.27.0",
75
+ "@iyulab/components": ">=1.52.0",
75
76
  "@lit/react": "^1.0.0",
76
77
  "react": "^18.0.0 || ^19.0.0"
77
78
  },
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: iyulab-flex-table
3
+ description: Spreadsheet-grade data grid web component (`<flex-table>`, built with Lit) with virtual scrolling, inline cell editing, validation, range selection, clipboard, undo/redo, sorting, filtering, pinned columns, and CSV/TSV/JSON/XLSX export, plus a React wrapper and OData / in-memory data-source hooks. Use when working with @iyulab/flex-table — defining columns, editing cells, handling grid events, wiring server-side paging with useODataSource or useArraySource, or styling the grid.
4
+ license: MIT
5
+ metadata:
6
+ author: iyulab
7
+ version: "0.40.1"
8
+ ---
9
+
10
+ # @iyulab/flex-table
11
+
12
+ A schema-agnostic data grid custom element (`<flex-table>`) for large datasets and cell-level editing.
13
+ Rows are plain objects (`DataRow = Record<string, unknown>`); columns describe how to show and edit them.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @iyulab/flex-table @iyulab/components
19
+ # React wrapper and hooks (optional peers)
20
+ npm install @lit/react react
21
+ ```
22
+
23
+ `@iyulab/components` is a peer dependency (locale, formatting, design tokens).
24
+
25
+ ## Entry points
26
+
27
+ | Import | Contents |
28
+ |---|---|
29
+ | `@iyulab/flex-table` | Registers `<flex-table>`; exports `FlexTable`, types, `exportData`, `renderCell`, `flexTableLocale`, `RowSelectionState`, `UndoStack`, `effectiveAlign` |
30
+ | `@iyulab/flex-table/react` | `FlexTableReact`, `useODataSource`, `useArraySource` |
31
+ | `@iyulab/flex-table/odata` | Pure helpers, no React: `buildODataQuery`, `buildSearchExpression`, `parseOrderBy` |
32
+ | `@iyulab/flex-table/array` | Pure helper, no React: `computeArrayView` |
33
+
34
+ ## Quick start
35
+
36
+ ```html
37
+ <flex-table id="table" style="height: 400px" show-row-numbers></flex-table>
38
+
39
+ <script type="module">
40
+ import '@iyulab/flex-table';
41
+
42
+ const table = document.getElementById('table');
43
+ table.columns = [
44
+ { key: 'name', label: 'Name', type: 'text', width: 200 },
45
+ { key: 'age', label: 'Age', type: 'number', width: 100 },
46
+ { key: 'active', label: 'Active', type: 'boolean', width: 80 },
47
+ ];
48
+ table.data = [
49
+ { name: 'Alice', age: 30, active: true },
50
+ { name: 'Bob', age: 25, active: false },
51
+ ];
52
+ table.addEventListener('cell-edit-commit', (e) => console.log(e.detail));
53
+ </script>
54
+ ```
55
+
56
+ ## Key concepts
57
+
58
+ **Give it a height.** The host is its own scroll container and virtualizes against its own height.
59
+ Without a height (or a constrained flex parent with `min-height: 0`), every row is rendered.
60
+
61
+ **Columns.** Each `ColumnDefinition` needs `key` and `label`. `type` picks the built-in
62
+ renderer/editor (`text`, `number`, `boolean`, `date`, `datetime`, `select`; unknown strings behave
63
+ as `text`). See `references/api.md` for every field.
64
+
65
+ - `format` changes only the displayed text (`'#,##0.00'`, `'0.00%'`, `'yyyy-MM-dd'`, or a function); sorting, filtering, editing and export keep the raw value.
66
+ - `render` replaces the cell content (`(value, row, col) => TemplateResult | string`) and wins over `format`.
67
+ - `editor` returns a Lit template containing an element with class `ft-editor`; its `.value` is committed.
68
+ - `validator` returns `null` when valid, or an error message (the edit is rejected and `validation-error` fires).
69
+ - `conditionalRules` applies `{ when, style }` rules in order; later matches override earlier ones.
70
+ - `pinned: 'left' | 'right'` freezes a column during horizontal scroll.
71
+
72
+ **Data is mutated in place.** Assigning a new array re-renders; changing a row object does not.
73
+ Use `updateRows([{ row, key, value }])` for programmatic edits (undoable, fires `batch-update`) or
74
+ `refreshData()` after an external in-place mutation.
75
+
76
+ **Editing and read-only.** `editable` defaults to `true`. For a read-only grid set
77
+ `table.editable = false` (a property, since a boolean attribute cannot express `false`) — Enter on a non-editable cell then fires
78
+ `row-activate`, the grid's "open this row" contract.
79
+
80
+ **Row selection is index-based.** Enable with `selectable` (`selection-mode="single|multi"`).
81
+ There is no row key, so set `clear-selection-on-data-change` when selection drives bulk actions and
82
+ `data` can be replaced. `selectWhere(predicate)` selects rows by content.
83
+
84
+ **Client vs server mode.** `data-mode="client"` (default) sorts and filters locally.
85
+ `data-mode="server"` only emits `sort-change` / `filter-change`; you supply already-processed rows.
86
+
87
+ **Filtering.** Programmatic `setFilter(key, (value, row) => boolean)`; `show-filters` adds a
88
+ **Filter…** entry to each column's header menu (text, number range, boolean, date/datetime range).
89
+ Both share one filter state.
90
+
91
+ **Undo/redo** covers edits, row and column operations, paste and comments (`max-undo-size`, default 100).
92
+
93
+ ## React
94
+
95
+ ```tsx
96
+ import { useRef } from 'react';
97
+ import { FlexTableReact, type FlexTable, type ColumnDefinition } from '@iyulab/flex-table/react';
98
+
99
+ interface Order { id: string; total: number; currency: string }
100
+
101
+ const columns: ColumnDefinition<Order>[] = [
102
+ { key: 'id', label: 'ID' },
103
+ { key: 'total', label: 'Total', render: (_v, row) => `${row.total} ${row.currency}` },
104
+ ];
105
+
106
+ function Orders({ orders }: { orders: Order[] }) {
107
+ const ref = useRef<FlexTable>(null);
108
+ return (
109
+ <FlexTableReact<Order>
110
+ ref={ref}
111
+ data={orders}
112
+ columns={columns}
113
+ selectable
114
+ onCellEditCommit={(e) => console.log(e.detail)}
115
+ />
116
+ );
117
+ }
118
+ ```
119
+
120
+ Server paging from OData or an in-memory array uses the same binding — see `references/react.md`.
121
+
122
+ ## Common pitfalls
123
+
124
+ - No height → no virtualization.
125
+ - `--ft-row-height` is read once at first render; change row height later via `rowHeight` / `row-height`.
126
+ - Styles for elements returned by `render` must be passed through the `stylesheets` property
127
+ (constructable `CSSStyleSheet[]`); document CSS does not cross the shadow boundary.
128
+ - `useODataSource` `fetcher` / `onUnauthorized` must be stable references (`useCallback`).
129
+ - Always render `error` from `useODataSource`; a failed request otherwise leaves the grid empty.
130
+
131
+ ## References
132
+
133
+ - [references/api.md](references/api.md) — properties, column definition, methods, events
134
+ - [references/react.md](references/react.md) — `FlexTableReact`, `useODataSource`, `useArraySource`, pure OData/array helpers
135
+ - [references/styling.md](references/styling.md) — CSS custom properties, density, keyboard shortcuts, localization
@@ -0,0 +1,173 @@
1
+ # flex-table API reference
2
+
3
+ All names below are members of the `FlexTable` class (`<flex-table>`).
4
+
5
+ ## Properties
6
+
7
+ | Property | Attribute | Type | Default | Notes |
8
+ |---|---|---|---|---|
9
+ | `columns` | — | `ColumnDefinition[]` | `[]` | Column definitions |
10
+ | `data` | — | `DataRow[]` | `[]` | Rows; mutated in place by the grid |
11
+ | `rowHeight` | `row-height` | `number` | `32` | Explicit value > `--ft-row-height` token > 32 |
12
+ | `showRowNumbers` | `show-row-numbers` | `boolean` | `false` | Sticky row-number column |
13
+ | `theme` | `theme` | `'light' \| 'dark' \| undefined` | auto | Auto follows `prefers-color-scheme` |
14
+ | `editable` | `editable` | `boolean` | `true` | `false` = read-only grid; Enter then fires `row-activate` |
15
+ | `showFilters` | `show-filters` | `boolean` | `false` | Adds **Filter…** to each column menu |
16
+ | `showContextMenu` | `show-context-menu` | `boolean` | `false` | Built-in cell context menu |
17
+ | `frozenRows` | `frozen-rows` | `number` | `0` | Rows kept visible at the top |
18
+ | `maxRows` | `max-rows` | `number` | `0` | 0 = unlimited; blocks `addRow()` and paste expansion |
19
+ | `maxUndoSize` | `max-undo-size` | `number` | `100` | Undo stack size |
20
+ | `selectable` | `selectable` | `boolean` | `false` | Row checkbox selection |
21
+ | `selectionMode` | `selection-mode` | `'single' \| 'multi'` | `'multi'` | |
22
+ | `dataMode` | `data-mode` | `'client' \| 'server'` | `'client'` | Server mode only emits sort/filter events |
23
+ | `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Summary row keyed by column key |
24
+ | `emptyMessage` | `empty-message` | `string` | `'No data'` | |
25
+ | `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | All rows hidden by filters |
26
+ | `loading` | `loading` | `boolean` | `false` | Loading overlay + `aria-busy` |
27
+ | `importEnabled` | `import-enabled` | `boolean` | `false` | Drag-and-drop `.xlsx` / `.csv` import |
28
+ | `clearUndoOnDataChange` | `clear-undo-on-data-change` | `boolean` | `false` | Replacing `data` clears undo history |
29
+ | `clearSelectionOnDataChange` | `clear-selection-on-data-change` | `boolean` | `false` | Replacing `data` clears row selection |
30
+ | `stylesheets` | — | `CSSStyleSheet[]` | `[]` | Adopted into the shadow root; styles `render` output |
31
+
32
+ Replacing `data` with the same array reference does not trigger the clear-on-change behaviors.
33
+
34
+ ### Read-only
35
+
36
+ | Getter | Type |
37
+ |---|---|
38
+ | `visibleColumns` | `ColumnDefinition[]` (non-hidden) |
39
+ | `filteredRowCount` | `number` |
40
+ | `filterKeys` | `string[]` |
41
+ | `sortCriteria` | `SortCriteria[]` — `{ key, direction: 'asc' \| 'desc' }` |
42
+ | `activeCell` / `editingCell` | `CellPosition \| null` — `{ row, col }` |
43
+ | `canUndo` / `canRedo` | `boolean` |
44
+
45
+ ## Column definition
46
+
47
+ `ColumnDefinition<T = DataRow>` fields:
48
+
49
+ | Field | Type | Notes |
50
+ |---|---|---|
51
+ | `key` | `string` | Property name in the row (required) |
52
+ | `label` | `string` | Header text (required) |
53
+ | `type` | `ColumnType` | `'text' \| 'number' \| 'boolean' \| 'date' \| 'datetime' \| 'select'`; other strings act as text |
54
+ | `width` / `minWidth` | `number` | px; `minWidth` defaults to 40 |
55
+ | `hidden` | `boolean` | |
56
+ | `sortable` | `boolean` | default `true` |
57
+ | `align` | `'start' \| 'center' \| 'end'` | Default from type: number → end, boolean → center, else start |
58
+ | `headerAlign` | `'start' \| 'center' \| 'end'` | Defaults to the cell alignment |
59
+ | `editable` | `boolean` | Per-column; the global `editable` still applies |
60
+ | `pinned` | `'left' \| 'right'` | |
61
+ | `options` | `string[] \| SelectOption[]` | For `select`; `SelectOption` = `{ label, value }` |
62
+ | `autocomplete` | `boolean \| 'strict'` | Suggest existing values; `'strict'` rejects others |
63
+ | `format` | `string \| (value, row, col) => string` | Display only |
64
+ | `render` | `CellRenderer<T>` | `(value, row, col) => TemplateResult \| string` |
65
+ | `editor` | `CellEditor<T>` | `(value, row, col) => TemplateResult` with an `.ft-editor` element |
66
+ | `validator` | `CellValidator<T>` | `(value, row, col) => string \| null \| undefined` |
67
+ | `conditionalRules` | `ConditionalRule<T>[]` | `{ when(value, row, col): boolean, style: CellStyle }` |
68
+
69
+ `CellStyle` = `{ background?, color?, fontWeight?: 'bold' | 'normal', fontStyle?: 'italic' | 'normal' }`.
70
+
71
+ ```ts
72
+ import { html } from 'lit';
73
+
74
+ table.columns = [
75
+ { key: 'total', label: 'Total', type: 'number', format: '#,##0.00' },
76
+ { key: 'status', label: 'Status',
77
+ conditionalRules: [{ when: (v) => v === 'overdue', style: { color: '#dc2626', fontWeight: 'bold' } }] },
78
+ { key: 'age', label: 'Age', type: 'number',
79
+ validator: (v) => (Number(v) < 0 ? 'Must be positive' : null) },
80
+ { key: 'color', label: 'Color',
81
+ editor: (v) => html`<input class="ft-editor" type="color" .value=${String(v ?? '#000000')}>` },
82
+ ];
83
+ ```
84
+
85
+ Custom editors: clicking another cell auto-commits; handle `@keydown` / `@blur` in the template for
86
+ Enter/Escape and blur-to-commit behavior.
87
+
88
+ ## Methods
89
+
90
+ ### Rows
91
+
92
+ | Method | Returns | Notes |
93
+ |---|---|---|
94
+ | `addRow(row?, index?)` | `DataRow \| null` | `null` when `maxRows` reached |
95
+ | `deleteRows(indices?)` | `void` | Data indices; default = selected rows |
96
+ | `updateRows(changes)` | `void` | `Array<{ row, key, value }>`, one undo step |
97
+ | `refreshData()` | `void` | Re-render after in-place mutation |
98
+
99
+ ### Columns
100
+
101
+ | Method | Returns |
102
+ |---|---|
103
+ | `addColumn(def, index?)` | `ColumnDefinition` |
104
+ | `deleteColumn(key)` | `void` (also clears its filter, sort and width) |
105
+ | `moveColumn(key, newIndex)` | `void` (index clamped) |
106
+ | `hideColumn(key)` / `showColumn(key)` | `void` |
107
+ | `getHiddenColumns()` | `ColumnDefinition[]` |
108
+ | `getColumnWidth(key)` | `number \| undefined` (resized width) |
109
+ | `selectColumn(colIndex)` | `void` (range-selects the column) |
110
+
111
+ ### Row selection
112
+
113
+ | Method | Notes |
114
+ |---|---|
115
+ | `selectAll()` / `deselectAll()` | `selectAll` is multi mode only |
116
+ | `selectWhere(predicate)` | `(row, dataIndex) => boolean` over the visible rows |
117
+ | `getSelectedRows()` | `{ selectedIndices, selectedRows }` |
118
+
119
+ ### Filtering, undo, comments, import/export
120
+
121
+ | Method | Notes |
122
+ |---|---|
123
+ | `setFilter(key, predicate)` / `removeFilter(key)` / `clearFilters()` | `predicate: (value, row) => boolean`; filters combine with AND |
124
+ | `undo()` / `redo()` / `clearUndoHistory()` | |
125
+ | `setComment(dataIndex, colKey, text)` | `null` or `''` removes; undoable |
126
+ | `getComment(dataIndex, colKey)` / `clearComments()` | |
127
+ | `importFromFile(file)` | `Promise<void>`; `.xlsx`, `.csv`, `.tsv`; fires `data-import` |
128
+ | `exportToString(format, { selectionOnly? })` | `string \| Uint8Array`; format `'csv' \| 'tsv' \| 'json' \| 'xlsx'` (xlsx returns bytes) |
129
+ | `exportToFile(format, filename?)` | Triggers a download |
130
+
131
+ ## Events
132
+
133
+ All are `CustomEvent`s with `bubbles: true, composed: true`. `row`/`index` are data indices.
134
+
135
+ | Event | `detail` |
136
+ |---|---|
137
+ | `cell-select` | `{ row, col }` (or `null`) |
138
+ | `cell-edit-start` | `{ row, col, key, value }` |
139
+ | `cell-edit-commit` | `{ row, col, key, oldValue, newValue }` |
140
+ | `cell-edit-cancel` | `{ row, col }` |
141
+ | `validation-error` | `{ row, col, key, value, error }` |
142
+ | `row-add` | `{ row, index }` |
143
+ | `row-delete` | `{ indices, rows }` |
144
+ | `row-activate` | `{ row, index, col, key }` — Enter on a non-editable cell |
145
+ | `row-reorder` | `{ from, to }` |
146
+ | `batch-update` | `{ changes }` |
147
+ | `column-add` | `{ column, index }` |
148
+ | `column-delete` | `{ column, key, index }` |
149
+ | `column-reorder` | `{ key, oldIndex, newIndex }` |
150
+ | `column-resize` | `{ key, width, colIndex }` |
151
+ | `column-select` | `{ colIndex, key, rowCount }` |
152
+ | `column-visibility-change` | `{ key, hidden }` |
153
+ | `sort-change` | `{ criteria }` |
154
+ | `filter-change` | `{ keys, filteredCount }` |
155
+ | `filter-error` | `{ error, row, filterKey }` |
156
+ | `selection-change` | `{ selectedIndices, selectedRows }` |
157
+ | `clipboard-copy` / `clipboard-cut` | `{ range, text }` (TSV) |
158
+ | `clipboard-paste` | `{ changes, addedRows }` |
159
+ | `clipboard-error` | `{ action: 'copy' \| 'paste', error }` |
160
+ | `fill-handle-apply` | `{ sourceRange, targetRange, cells }` |
161
+ | `find-replace` | `{ type: 'replace' \| 'replace-all', cells }` |
162
+ | `comment-change` | `{ dataIndex, colKey, text }` |
163
+ | `data-import` | `{ count }` |
164
+ | `undo-state-change` | `{ canUndo, canRedo }` |
165
+ | `context-menu` | `{ x, y, row, col, key, value, rowData }` — cancelable; `preventDefault()` suppresses the built-in menu |
166
+ | `header-context-menu` | `{ key, label, x, y }` |
167
+
168
+ ## Other exports
169
+
170
+ `exportData(rows, columns, format)`, `renderCell`, `effectiveAlign(col)`, `RowSelectionState`,
171
+ `UndoStack`, `flexTableLocale`, and the types `CellPosition`, `CellRange`, `SortCriteria`,
172
+ `SortDirection`, `ColumnFilter`, `FilterPredicate`, `FilterErrorCallback`, `ExportFormat`,
173
+ `UndoAction`, `FlexTableMessageKey`.
@@ -0,0 +1,177 @@
1
+ # React: `@iyulab/flex-table/react`
2
+
3
+ Requires the optional peers `react` and `@lit/react`.
4
+
5
+ ## `FlexTableReact`
6
+
7
+ A `@lit/react` wrapper around `<flex-table>`, generic over the row type `T` (default `DataRow`).
8
+ Every element property is a prop; `ref` resolves to the `FlexTable` instance, so all methods
9
+ (`addRow`, `deleteRows`, `setFilter`, `selectWhere`, …) are reachable imperatively.
10
+
11
+ ```tsx
12
+ import { useRef } from 'react';
13
+ import { FlexTableReact, type FlexTable, type ColumnDefinition } from '@iyulab/flex-table/react';
14
+
15
+ const columns: ColumnDefinition<Order>[] = [
16
+ { key: 'id', label: 'ID' },
17
+ { key: 'total', label: 'Total', type: 'number', format: '#,##0.00' },
18
+ ];
19
+
20
+ function Orders({ orders }: { orders: Order[] }) {
21
+ const ref = useRef<FlexTable>(null);
22
+ return (
23
+ <>
24
+ <button onClick={() => ref.current?.addRow({ id: '', total: 0 })}>Add</button>
25
+ <FlexTableReact<Order> ref={ref} data={orders} columns={columns} selectable
26
+ onSelectionChange={(e) => console.log(e.detail.selectedRows)} />
27
+ </>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ### Mapped event props
33
+
34
+ | Prop | Event | Prop | Event |
35
+ |---|---|---|---|
36
+ | `onCellSelect` | `cell-select` | `onColumnSelect` | `column-select` |
37
+ | `onCellEditStart` | `cell-edit-start` | `onColumnAdd` | `column-add` |
38
+ | `onCellEditCommit` | `cell-edit-commit` | `onColumnDelete` | `column-delete` |
39
+ | `onCellEditCancel` | `cell-edit-cancel` | `onColumnReorder` | `column-reorder` |
40
+ | `onValidationError` | `validation-error` | `onColumnResize` | `column-resize` |
41
+ | `onSortChange` | `sort-change` | `onSelectionChange` | `selection-change` |
42
+ | `onFilterChange` | `filter-change` | `onClipboardCopy` | `clipboard-copy` |
43
+ | `onFilterError` | `filter-error` | `onClipboardCut` | `clipboard-cut` |
44
+ | `onRowAdd` | `row-add` | `onClipboardPaste` | `clipboard-paste` |
45
+ | `onRowDelete` | `row-delete` | `onClipboardError` | `clipboard-error` |
46
+ | `onRowActivate` | `row-activate` | `onUndoStateChange` | `undo-state-change` |
47
+ | `onBatchUpdate` | `batch-update` | `onContextMenu` | `context-menu` |
48
+ | `onRowReorder` | `row-reorder` | `onHeaderContextMenu` | `header-context-menu` |
49
+ | `onColumnVisibilityChange` | `column-visibility-change` | `onCommentChange` | `comment-change` |
50
+ | `onDataImport` | `data-import` | `onFillHandleApply` | `fill-handle-apply` |
51
+ | `onFindReplace` | `find-replace` | | |
52
+
53
+ Every custom event of `<flex-table>` has a prop.
54
+
55
+ Type exports from this entry: `FlexTable`, `FlexTableReactProps`, `ColumnDefinition`, `DataRow`,
56
+ `ColumnType`, `ColumnAlign`, `CellRenderer`, `CellEditor`, `CellValidator`, `ConditionalRule`,
57
+ `SelectionMode`, `DataMode`, `UseODataSourceOptions`, `UseODataSourceResult`,
58
+ `UseArraySourceOptions`, `UseArraySourceResult`.
59
+
60
+ ## `useODataSource(url, options)`
61
+
62
+ Fetches paged, sorted, searched rows from an OData v4 endpoint. Bind the result to a
63
+ server-mode table:
64
+
65
+ ```tsx
66
+ import { useCallback } from 'react';
67
+ import { FlexTableReact, useODataSource } from '@iyulab/flex-table/react';
68
+
69
+ function Orders() {
70
+ const fetcher = useCallback((input: string, init: RequestInit) => fetch(input, init), []);
71
+ const source = useODataSource<Order>('/api/orders', { pageSize: 20, fetcher });
72
+
73
+ return (
74
+ <>
75
+ <input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
76
+ {source.error && <p role="alert">{source.error}</p>}
77
+ <FlexTableReact<Order>
78
+ dataMode="server"
79
+ columns={columns}
80
+ data={source.data}
81
+ loading={source.loading}
82
+ onSortChange={source.onSortChange}
83
+ clearSelectionOnDataChange
84
+ />
85
+ <button disabled={source.page === 0} onClick={() => source.setPage(source.page - 1)}>Prev</button>
86
+ <span>{source.page + 1} / {Math.max(1, Math.ceil(source.totalCount / 20))}</span>
87
+ <button onClick={() => source.setPage(source.page + 1)}>Next</button>
88
+ </>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ### Options
94
+
95
+ | Option | Default | Notes |
96
+ |---|---|---|
97
+ | `pageSize` | `20` | |
98
+ | `defaultOrderBy` | — | `$orderby` string, e.g. `'name asc'` |
99
+ | `initialPage` | `0` | Zero-based; first render only |
100
+ | `initialSearch` | `''` | First render only |
101
+ | `initialSort` | — | `SortCriteria[]`; overrides `defaultOrderBy`; `[]` means no sort |
102
+ | `fixedFilter` | — | odata-query filter object, always applied; value change resets to page 0 (compared by value) |
103
+ | `baseUrl` | `window.location.origin` | For proxy/BFF setups |
104
+ | `fetcher` | global `fetch` | `(input, init) => Promise<Response>`; keep stable |
105
+ | `onUnauthorized` | — | `(response) => void` on 401/403; keep stable |
106
+ | `enabled` | `true` | While `false`: no request, `loading` stays `true`, `refresh()` is a no-op, in-flight request is cancelled |
107
+
108
+ ### Result
109
+
110
+ | Field | Notes |
111
+ |---|---|
112
+ | `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
113
+ | `loading` | Request in flight |
114
+ | `error` | `string \| null` — render it |
115
+ | `page`, `setPage` | Zero-based |
116
+ | `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
117
+ | `search`, `setSearch` | Literal text (resets to page 0) |
118
+ | `refresh` | Re-run the current request |
119
+
120
+ Search is literal: each whitespace-separated token becomes a quoted phrase joined with `AND`
121
+ (`red shirt` → `"red" AND "shirt"`); double quotes are stripped. If the result set shrinks below
122
+ the current page, the page moves down to the last existing page.
123
+
124
+ Use `enabled` when the query depends on an async value:
125
+
126
+ ```tsx
127
+ const source = useODataSource('/api/orders', {
128
+ fixedFilter: season ? { Season: season } : undefined,
129
+ enabled: season !== undefined,
130
+ });
131
+ ```
132
+
133
+ ## `useArraySource(data, options)`
134
+
135
+ Search, sort and paging over an in-memory array, returning **the same shape** as
136
+ `useODataSource` — the same `dataMode="server"` binding works with either.
137
+
138
+ ```tsx
139
+ import { useMemo } from 'react';
140
+ import { useArraySource } from '@iyulab/flex-table/react';
141
+
142
+ const joined = useMemo(
143
+ () => prices.map((p) => ({ ...p, productName: productsById[p.productId]?.name ?? '' })),
144
+ [prices, productsById],
145
+ );
146
+ const source = useArraySource(joined, { pageSize: 20, columns });
147
+ ```
148
+
149
+ | Option | Default | Notes |
150
+ |---|---|---|
151
+ | `pageSize` | `20` | |
152
+ | `defaultOrderBy` | — | Same syntax as `useODataSource` |
153
+ | `initialPage` / `initialSearch` / `initialSort` | `0` / `''` / — | Same first-render-only contract |
154
+ | `columns` | — | Enables value-aware sort (number/date/boolean); otherwise text sort |
155
+ | `searchFields` | all row values | `(row) => Array<string \| number \| boolean \| null \| undefined>` |
156
+
157
+ Differences: `totalCount` is the count after search, `loading` is always `false`, `error` always
158
+ `null`, and `refresh` is a no-op. The page is clamped down if the array shrinks.
159
+
160
+ ## Without React
161
+
162
+ ```ts
163
+ import { buildODataQuery, buildSearchExpression, parseOrderBy } from '@iyulab/flex-table/odata';
164
+ import { computeArrayView } from '@iyulab/flex-table/array';
165
+
166
+ buildSearchExpression('red shirt'); // '"red" AND "shirt"' ('' → undefined)
167
+ parseOrderBy('name desc'); // [{ key: 'name', direction: 'desc' }]
168
+ buildODataQuery({ page: 2, pageSize: 20, search: 'red', fixedFilter: { IsActive: true } });
169
+ // '?$filter=...&$count=true&$top=20&$skip=40&$search=...'
170
+
171
+ const { data, totalCount } = computeArrayView(rows, {
172
+ search: '', sortCriteria: [], page: 0, pageSize: 20, columns,
173
+ });
174
+ ```
175
+
176
+ `buildODataQuery` state fields: `page`, `pageSize`, and optional `sortCriteria`, `defaultOrderBy`,
177
+ `search`, `fixedFilter` — exactly what the hook sends per request.
@@ -0,0 +1,108 @@
1
+ # Styling, keyboard, localization
2
+
3
+ ## Sizing
4
+
5
+ The host is the scroll container and virtualizes against its own height:
6
+
7
+ ```css
8
+ flex-table { height: 400px; }
9
+ /* or inside a flex column */
10
+ .page { height: 100%; display: flex; flex-direction: column; }
11
+ .page flex-table { flex: 1 1 auto; min-height: 0; }
12
+ ```
13
+
14
+ ## Theming
15
+
16
+ Every `--ft-*` color falls back to an `@iyulab/components` design token, so loading that token sheet
17
+ makes the grid follow the app theme (e.g. `:root { --u-primary-color: #7b1fa2; }`). Without the sheet,
18
+ literal fallbacks and the built-in dark theme (`prefers-color-scheme` or `theme="dark"`) still apply.
19
+
20
+ Override individual properties to make the table differ from the rest of the app:
21
+
22
+ | Group | Custom properties |
23
+ |---|---|
24
+ | Font | `--ft-font-family`, `--ft-font-size` |
25
+ | Surfaces / text | `--ft-bg`, `--ft-text-color`, `--ft-border-color`, `--ft-editor-bg`, `--ft-empty-color` |
26
+ | Header | `--ft-header-bg`, `--ft-header-hover-bg`, `--ft-header-text-color`, `--ft-sort-indicator-color` |
27
+ | Rows | `--ft-row-even-bg`, `--ft-row-odd-bg`, `--ft-row-hover-bg`, `--ft-row-odd-hover-bg` |
28
+ | Accent | `--ft-active-color`, `--ft-selection-bg`, `--ft-bool-color` |
29
+ | State overlays | `--ft-invalid-color`, `--ft-drop-color`, `--ft-find-color` (translucent backgrounds derive from these) |
30
+
31
+ ```css
32
+ flex-table {
33
+ --ft-font-size: 13px;
34
+ --ft-active-color: #1a73e8;
35
+ --ft-row-odd-bg: #fafafa;
36
+ }
37
+ ```
38
+
39
+ ### Styling custom cell content
40
+
41
+ `render` output lives in the shadow root, so page CSS does not reach it. Pass constructable
42
+ stylesheets instead (reassigning replaces the previous set):
43
+
44
+ ```ts
45
+ const sheet = new CSSStyleSheet();
46
+ sheet.replaceSync('.badge { padding: 0 6px; border-radius: 4px; }');
47
+ table.stylesheets = [sheet];
48
+ ```
49
+
50
+ ## Density and header hierarchy
51
+
52
+ ```css
53
+ flex-table {
54
+ --ft-row-height: 28px;
55
+ --ft-cell-padding-block: 4px;
56
+ --ft-cell-padding-inline: 8px;
57
+ --ft-header-font-size: 13px; /* defaults to --ft-font-size */
58
+ --ft-header-font-weight: 600;
59
+ }
60
+ ```
61
+
62
+ - `--ft-row-height` is read once at first render and only in `px`; to change it later set the
63
+ `rowHeight` property / `row-height` attribute (which always win over the token).
64
+ - Cells are single-line. When reducing row height, reduce `--ft-cell-padding-block` too
65
+ (`padding-block × 2 + line box ≤ row height`), or text is clipped.
66
+
67
+ ## Keyboard
68
+
69
+ | Key | Action |
70
+ |---|---|
71
+ | Arrows / Tab / Shift+Tab | Move between cells |
72
+ | Home / End, Ctrl+Home / Ctrl+End | Row start/end, table start/end |
73
+ | Shift+Arrow, Shift+Click | Extend range selection |
74
+ | Ctrl+Click header | Select column |
75
+ | Enter / F2 | Edit (Enter on a non-editable cell fires `row-activate`) |
76
+ | Typing a printable character | Start editing an editable cell |
77
+ | Escape | Cancel edit / clear selection / close find panel |
78
+ | Delete / Backspace | Clear selected cells |
79
+ | Ctrl+C / Ctrl+X / Ctrl+V | Copy / cut / paste as TSV (Excel / Google Sheets compatible) |
80
+ | Ctrl+D / Ctrl+R | Fill down / fill right |
81
+ | Ctrl+F / Ctrl+H | Find / find and replace |
82
+ | Ctrl+Z, Ctrl+Y or Ctrl+Shift+Z | Undo, redo |
83
+ | Alt+ArrowLeft / Alt+ArrowRight | Resize current column |
84
+ | Enter / Space on a column menu button | Open the column menu (arrows, Home/End navigate; Escape closes) |
85
+
86
+ Ctrl also matches Cmd on macOS. Editing shortcuts are ignored when `editable` is `false`.
87
+
88
+ ## Localization
89
+
90
+ The grid's own chrome (column menu, filters, find/replace, context menu, empty states) uses a locale
91
+ namespace. English and Korean are built in and follow the active `@iyulab/components` locale:
92
+
93
+ ```ts
94
+ import { Locale } from '@iyulab/components';
95
+ Locale.set('ko');
96
+ ```
97
+
98
+ Add a language or reword strings with partial tables; `FlexTableMessageKey` lists the valid keys:
99
+
100
+ ```ts
101
+ import { flexTableLocale } from '@iyulab/flex-table';
102
+
103
+ flexTableLocale.register('ja', { contains: '含む', startsWith: '前方一致' });
104
+ flexTableLocale.register('en', { replaceAll: 'Replace everything' });
105
+ flexTableLocale.register('ja', { columnMenuFor: '{header} の列メニュー' }); // {header} placeholder
106
+ ```
107
+
108
+ `emptyMessage` and `noMatchingMessage` can also be set per table.