flexi-datagrid 1.2.12 → 1.2.14
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/README.md +56 -7
- package/dist/DataTable/components/ConditionFilterPanel.d.ts +8 -0
- package/dist/DataTable/constants.d.ts +8 -0
- package/dist/DataTable/hooks/useColumnFilters.d.ts +10 -2
- package/dist/DataTable/hooks/useDataTableState.d.ts +7 -1
- package/dist/DataTable/index.d.ts +1 -1
- package/dist/DataTable/types.d.ts +17 -0
- package/dist/DataTable/utils/records.d.ts +6 -1
- package/dist/flexi-datagrid.css +1 -1
- package/dist/index.es.js +737 -360
- package/dist/index.umd.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A modern, highly customizable, and feature-rich React data grid component design
|
|
|
9
9
|
- **Sticky Header:** The header row stays fixed in place while the body scrolls vertically — give the grid a bounded height via `containerStyle` to enable vertical scrolling.
|
|
10
10
|
- **Table Borders:** Optional full grid borders via the `bordered` prop for a classic spreadsheet look.
|
|
11
11
|
- **Multi-State Sorting:** Clickable column headers that support Ascending (asc), Descending (desc), and Unsorted states.
|
|
12
|
-
- **Built-in Filtering:**
|
|
12
|
+
- **Built-in Filtering:** Every filterable column's popup has two tabs — a **Filter** tab with an AG Grid-style condition builder (contains/equals/greater than/before/in range/blank, up to two conditions joined by AND/OR, per column data type) and a **Select** tab with the original multi-select checkbox list of unique values. Both apply live as you type/check, and the popup renders through a portal so it always floats above your grid content instead of being clipped by the table's scroll container.
|
|
13
13
|
- **Row Grouping (Drag-and-Drop):** Drag column headers into the grouping zone to group your dataset hierarchically with expand/collapse support.
|
|
14
14
|
- **Column Reordering (Drag-and-Drop):** Drag any header and drop it on another to reorder columns, AG Grid-style. Column groups move as a unit; columns can be reordered within their own group or at the top level.
|
|
15
15
|
- **Column Resizing:** Drag the handle on the right edge of a header to resize a column live. Supports per-column `minWidth`/`maxWidth`, opt-out via `resizable: false`, and an `onColumnResize` callback.
|
|
@@ -66,15 +66,17 @@ const data: Employee[] = [
|
|
|
66
66
|
];
|
|
67
67
|
|
|
68
68
|
const columns: ColumnConfig<Employee>[] = [
|
|
69
|
-
{ key: 'name', header: 'Name', sortable: true, filterable: true, pinned: 'left', minWidth: 160, maxWidth: 500,
|
|
69
|
+
{ key: 'name', header: 'Name', sortable: true, filterable: true, filterType: 'text', pinned: 'left', minWidth: 160, maxWidth: 500,
|
|
70
70
|
style: { width: "400px", backgroundColor: "#f9f9f9" },
|
|
71
71
|
headerStyle: { fontWeight: "bold", color: "#333" } },
|
|
72
|
-
{ key: 'department', header: 'Department', sortable: true, filterable: true },
|
|
72
|
+
{ key: 'department', header: 'Department', sortable: true, filterable: true, filterType: 'text' },
|
|
73
73
|
{ key: 'role', header: 'Role', sortable: true, filterable: true, resizable: false },
|
|
74
74
|
{
|
|
75
75
|
key: 'salary',
|
|
76
76
|
header: 'Salary',
|
|
77
77
|
sortable: true,
|
|
78
|
+
filterable: true,
|
|
79
|
+
filterType: 'number',
|
|
78
80
|
pinned: 'right',
|
|
79
81
|
render: (value) => `$${value.toLocaleString()}`,
|
|
80
82
|
},
|
|
@@ -131,7 +133,8 @@ Each entry in `columns` describes one column (or a group of columns, via `childr
|
|
|
131
133
|
| `style` / `headerStyle` | `CSSProperties` | Inline styles for body cells / the header cell (e.g. fixed `width`). |
|
|
132
134
|
| `render` | `(value, record) => ReactNode` | Custom cell renderer. Falls back to the raw value if omitted. |
|
|
133
135
|
| `getValue` | `(record) => any` | Custom accessor used for sorting, filtering, and grouping when the value isn't a simple `record[key]` lookup. |
|
|
134
|
-
| `filterable` | `boolean` | Shows a filter icon in the header that opens a
|
|
136
|
+
| `filterable` | `boolean` | Shows a filter icon in the header that opens a popup with a condition-filter tab and a multi-select "values" tab. See [Filtering](#filtering). |
|
|
137
|
+
| `filterType` | `'text' \| 'number' \| 'date' \| 'boolean'` | Which operator set the condition filter tab shows for this column. Auto-detected from the first non-null value when omitted (so it's optional for existing columns). |
|
|
135
138
|
| `sortable` | `boolean` | Makes the header clickable, cycling through ascending → descending → unsorted. |
|
|
136
139
|
| `pinned` | `boolean \| 'left' \| 'right'` | Pins the column to an edge by default (`true` is shorthand for `'left'`). Still toggleable per-column from the UI. |
|
|
137
140
|
| `pinnedRender` | `(value, record) => ReactNode` | Overrides `render` specifically for rows supplied via `pinnedBottomRowData` (e.g. render a "Total" label or bold sum). Falls back to `render` when omitted. |
|
|
@@ -172,10 +175,56 @@ Each entry in `columns` describes one column (or a group of columns, via `childr
|
|
|
172
175
|
| `defaultPageSize` | `number` | Initial number of rows per page when `pagination` is enabled. Defaults to `10`. |
|
|
173
176
|
| `pageSizeOptions` | `number[]` | Options shown in the rows-per-page selector. Defaults to `[10, 25, 50, 100]`. |
|
|
174
177
|
| `onPageChange` | `(page: number, pageSize: number) => void` | Called whenever the current page or page size changes. |
|
|
178
|
+
| `onConditionFilterChange` | `(conditionFilters: Record<string, ConditionFilter>) => void` | Called whenever a column's condition filter (the "Filter" tab) changes, with the full map of active condition filters keyed by column key. |
|
|
175
179
|
|
|
176
180
|
### Filtering
|
|
177
181
|
|
|
178
|
-
Any column with `filterable: true` gets a filter icon in its header. Clicking it opens a
|
|
182
|
+
Any column with `filterable: true` gets a filter icon in its header. Clicking it opens a popup with two tabs, similar to AG Grid:
|
|
183
|
+
|
|
184
|
+
- **Filter** (the default tab) — an operator-driven condition builder. Pick an operator (e.g. "Contains", "Greater than", "Before", "In range", "Blank"), type a value, and the grid filters live as you type (debounced) or as soon as you change the operator/checkbox/radio — there's no separate "Apply" button to click. Check **Add another condition** to add a second operator/value row combined with the first via **AND**/**OR**. Use **Clear** to remove the condition filter for that column.
|
|
185
|
+
- **Select** — the original searchable, multi-select checkbox list of the column's unique values (derived from `data`, via `getValue` if provided).
|
|
186
|
+
|
|
187
|
+
Both tabs' filters combine with AND across different columns. Within one column, a condition filter's own two conditions combine via the AND/OR you chose, and the Select tab's checked values combine via OR with each other (so a Select filter and a Filter-tab condition on the *same* column both have to match, same as AG Grid's "Filter" + "Set Filter" combination).
|
|
188
|
+
|
|
189
|
+
Which operators are offered depends on the column's type — `'text'` (contains/not contains/equals/not equal/starts with/ends with/blank/not blank), `'number'` (equals/not equal/greater than/greater than or equal/less than/less than or equal/in range/blank/not blank), or `'date'` (equals/not equal/before/after/in range/blank/not blank). Set it explicitly via `filterType` on the column, or leave it out and the grid infers it from the first non-null value in `data` for that column (numbers → `'number'`, `Date` instances → `'date'`, booleans → `'boolean'`, otherwise `'text'`).
|
|
190
|
+
|
|
191
|
+
The popup is rendered through a portal anchored to the grid's own root element, so it overlays the grid instead of being clipped by the table's horizontal scroll container — similar to an Excel-style filter panel.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { DataTable, type ColumnConfig, type ConditionFilter } from 'flexi-datagrid';
|
|
195
|
+
|
|
196
|
+
interface Employee {
|
|
197
|
+
id: number;
|
|
198
|
+
name: string;
|
|
199
|
+
department: string;
|
|
200
|
+
salary: number;
|
|
201
|
+
hiredAt: Date;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const columns: ColumnConfig<Employee>[] = [
|
|
205
|
+
{ key: 'name', header: 'Name', filterable: true, filterType: 'text' },
|
|
206
|
+
{ key: 'department', header: 'Department', filterable: true, filterType: 'text' },
|
|
207
|
+
{ key: 'salary', header: 'Salary', filterable: true, filterType: 'number' },
|
|
208
|
+
{
|
|
209
|
+
key: 'hiredAt',
|
|
210
|
+
header: 'Hired',
|
|
211
|
+
filterable: true,
|
|
212
|
+
filterType: 'date',
|
|
213
|
+
render: (value: Date) => value.toLocaleDateString(),
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
<DataTable<Employee>
|
|
218
|
+
columns={columns}
|
|
219
|
+
data={data}
|
|
220
|
+
rowKey="id"
|
|
221
|
+
onConditionFilterChange={(conditionFilters: Record<string, ConditionFilter>) =>
|
|
222
|
+
console.log('condition filters', conditionFilters)
|
|
223
|
+
}
|
|
224
|
+
/>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
With the columns above, opening the **Salary** header's filter and choosing "Greater than" with `80000`, then checking **Add another condition** and choosing "Less than" with `120000` (combinator `AND`), keeps only rows with `80000 < salary < 120000` — no clicking required beyond typing the two values.
|
|
179
228
|
|
|
180
229
|
### Sorting
|
|
181
230
|
|
|
@@ -403,8 +452,8 @@ const data: Employee[] = [
|
|
|
403
452
|
];
|
|
404
453
|
|
|
405
454
|
const columns: ColumnConfig<Employee>[] = [
|
|
406
|
-
{ key: 'city', header: 'City', sortable: true, filterable: true, minWidth: 100, maxWidth: 240 },
|
|
407
|
-
{ key: 'department', header: 'Department', sortable: true, filterable: true, minWidth: 120 },
|
|
455
|
+
{ key: 'city', header: 'City', sortable: true, filterable: true, filterType: 'text', minWidth: 100, maxWidth: 240 },
|
|
456
|
+
{ key: 'department', header: 'Department', sortable: true, filterable: true, filterType: 'text', minWidth: 120 },
|
|
408
457
|
{
|
|
409
458
|
key: 'name',
|
|
410
459
|
header: 'Name',
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AG Grid-style "Filter" tab: up to two operator-driven conditions joined by AND/OR.
|
|
3
|
+
* Applies live (debounced while typing, instantly on operator/combinator changes) so it behaves
|
|
4
|
+
* like the existing "Select" tab's checkboxes rather than requiring an extra click to take effect.
|
|
5
|
+
*/
|
|
6
|
+
export declare function ConditionFilterPanel({ columnKey }: {
|
|
7
|
+
columnKey: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FilterOperator, FilterType } from './types';
|
|
1
2
|
export declare const DEFAULT_ROW_HEIGHT = 40;
|
|
2
3
|
export declare const DEFAULT_COL_WIDTH = 150;
|
|
3
4
|
export declare const MIN_COL_WIDTH = 60;
|
|
@@ -9,3 +10,10 @@ export declare const DEFAULT_PAGE_SIZE = 10;
|
|
|
9
10
|
export declare const DEFAULT_PAGE_SIZE_OPTIONS: number[];
|
|
10
11
|
export declare const EMPTY_STRINGS: string[];
|
|
11
12
|
export declare const EMPTY_ARRAY: never[];
|
|
13
|
+
export interface FilterOperatorOption {
|
|
14
|
+
value: FilterOperator;
|
|
15
|
+
label: string;
|
|
16
|
+
/** How many free-text value inputs this operator needs (0 = none, 2 = "in range"). */
|
|
17
|
+
valueCount: 0 | 1 | 2;
|
|
18
|
+
}
|
|
19
|
+
export declare const FILTER_OPERATORS_BY_TYPE: Record<FilterType, FilterOperatorOption[]>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
|
-
import type { ColumnConfig } from '../types';
|
|
2
|
+
import type { ColumnConfig, ConditionFilter, FilterType } from '../types';
|
|
3
|
+
export type FilterTab = 'condition' | 'values';
|
|
3
4
|
interface ColumnFiltersOptions<T> {
|
|
4
5
|
data: T[];
|
|
5
6
|
columnsByKey: Map<string, ColumnConfig<T>>;
|
|
6
7
|
gridRootRef: RefObject<HTMLElement | null>;
|
|
8
|
+
onConditionFilterChange?: (conditionFilters: Record<string, ConditionFilter>) => void;
|
|
7
9
|
}
|
|
8
|
-
export declare function useColumnFilters<T>({ data, columnsByKey, gridRootRef }: ColumnFiltersOptions<T>): {
|
|
10
|
+
export declare function useColumnFilters<T>({ data, columnsByKey, gridRootRef, onConditionFilterChange }: ColumnFiltersOptions<T>): {
|
|
9
11
|
activeFilterKey: string | null;
|
|
12
|
+
filterTab: FilterTab;
|
|
13
|
+
setFilterTab: import("react").Dispatch<import("react").SetStateAction<FilterTab>>;
|
|
10
14
|
selectedFilters: Record<string, unknown[]>;
|
|
15
|
+
conditionFilters: Record<string, ConditionFilter>;
|
|
11
16
|
filterSearchQuery: string;
|
|
12
17
|
setFilterSearchQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
13
18
|
filterPosition: {
|
|
@@ -16,10 +21,13 @@ export declare function useColumnFilters<T>({ data, columnsByKey, gridRootRef }:
|
|
|
16
21
|
} | null;
|
|
17
22
|
dropdownRef: RefObject<HTMLDivElement | null>;
|
|
18
23
|
filteredUniqueValues: unknown[];
|
|
24
|
+
activeColumnFilterType: FilterType;
|
|
19
25
|
registerFilterButton: (key: string, el: HTMLButtonElement | null) => void;
|
|
20
26
|
toggleFilterDropdown: (columnKey: string) => void;
|
|
21
27
|
handleFilterChange: (columnKey: string, value: unknown) => void;
|
|
22
28
|
clearFilter: (columnKey: string) => void;
|
|
29
|
+
setConditionFilter: (columnKey: string, filter: ConditionFilter | null) => void;
|
|
30
|
+
clearConditionFilter: (columnKey: string) => void;
|
|
23
31
|
};
|
|
24
32
|
export type ColumnFilters = ReturnType<typeof useColumnFilters>;
|
|
25
33
|
export {};
|
|
@@ -3,7 +3,7 @@ import type { ColumnConfig, DataTableProps } from '../types';
|
|
|
3
3
|
* Owns all of the grid's state and derived data. `DataTable` publishes the result through
|
|
4
4
|
* `DataTableContext`, so child components read what they need instead of receiving it as props.
|
|
5
5
|
*/
|
|
6
|
-
export declare function useDataTableState<T>({ columns, data, rowKey, actions, isLoading, groupBy, defaultGroupBy, onPinChange, onGroupChange, pinnedBottomRowData, pinnedRowClassName, rowHeight, overscan, onColumnResize, onColumnReorder, rowClassRules, onRowClick, rowSpanColumns, controlPanel, defaultDisplayPinnedIcon, defaultHiddenColumns, onColumnVisibilityChange, pagination, defaultPageSize, pageSizeOptions, onPageChange, }: DataTableProps<T>): {
|
|
6
|
+
export declare function useDataTableState<T>({ columns, data, rowKey, actions, isLoading, groupBy, defaultGroupBy, onPinChange, onGroupChange, pinnedBottomRowData, pinnedRowClassName, rowHeight, overscan, onColumnResize, onColumnReorder, rowClassRules, onRowClick, rowSpanColumns, controlPanel, defaultDisplayPinnedIcon, defaultHiddenColumns, onColumnVisibilityChange, pagination, defaultPageSize, pageSizeOptions, onPageChange, onConditionFilterChange, }: DataTableProps<T>): {
|
|
7
7
|
gridRootRef: import("react").RefObject<HTMLDivElement | null>;
|
|
8
8
|
tableWrapRef: import("react").RefObject<HTMLDivElement | null>;
|
|
9
9
|
rowKey: keyof T | ((record: T) => string);
|
|
@@ -59,7 +59,10 @@ export declare function useDataTableState<T>({ columns, data, rowKey, actions, i
|
|
|
59
59
|
handleSort: (columnKey: string) => void;
|
|
60
60
|
filters: {
|
|
61
61
|
activeFilterKey: string | null;
|
|
62
|
+
filterTab: import("./useColumnFilters").FilterTab;
|
|
63
|
+
setFilterTab: import("react").Dispatch<import("react").SetStateAction<import("./useColumnFilters").FilterTab>>;
|
|
62
64
|
selectedFilters: Record<string, unknown[]>;
|
|
65
|
+
conditionFilters: Record<string, import("..").ConditionFilter>;
|
|
63
66
|
filterSearchQuery: string;
|
|
64
67
|
setFilterSearchQuery: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
65
68
|
filterPosition: {
|
|
@@ -68,10 +71,13 @@ export declare function useDataTableState<T>({ columns, data, rowKey, actions, i
|
|
|
68
71
|
} | null;
|
|
69
72
|
dropdownRef: import("react").RefObject<HTMLDivElement | null>;
|
|
70
73
|
filteredUniqueValues: unknown[];
|
|
74
|
+
activeColumnFilterType: import("..").FilterType;
|
|
71
75
|
registerFilterButton: (key: string, el: HTMLButtonElement | null) => void;
|
|
72
76
|
toggleFilterDropdown: (columnKey: string) => void;
|
|
73
77
|
handleFilterChange: (columnKey: string, value: unknown) => void;
|
|
74
78
|
clearFilter: (columnKey: string) => void;
|
|
79
|
+
setConditionFilter: (columnKey: string, filter: import("..").ConditionFilter | null) => void;
|
|
80
|
+
clearConditionFilter: (columnKey: string) => void;
|
|
75
81
|
};
|
|
76
82
|
currentPage: number;
|
|
77
83
|
pageSize: number;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DataTable } from './DataTable';
|
|
2
|
-
export type { ColumnConfig, DataTableProps, PinSide, SortDirection } from './types';
|
|
2
|
+
export type { ColumnConfig, ConditionFilter, DataTableProps, FilterCondition, FilterOperator, FilterType, PinSide, SortDirection, } from './types';
|
|
@@ -9,6 +9,8 @@ export interface ColumnConfig<T> {
|
|
|
9
9
|
render?: (value: any, record: T) => ReactNode;
|
|
10
10
|
pinnedRender?: (value: any, record: T) => ReactNode;
|
|
11
11
|
filterable?: boolean;
|
|
12
|
+
/** Operator set shown in the condition filter tab. Auto-detected from the first non-null value when omitted. */
|
|
13
|
+
filterType?: FilterType;
|
|
12
14
|
sortable?: boolean;
|
|
13
15
|
pinned?: boolean | 'left' | 'right';
|
|
14
16
|
children?: ColumnConfig<T>[];
|
|
@@ -23,6 +25,20 @@ export interface SortState {
|
|
|
23
25
|
key: string | null;
|
|
24
26
|
direction: SortDirection;
|
|
25
27
|
}
|
|
28
|
+
/** Data type driving which operators the condition filter offers for a column. */
|
|
29
|
+
export type FilterType = 'text' | 'number' | 'date' | 'boolean';
|
|
30
|
+
export type FilterOperator = 'contains' | 'notContains' | 'equals' | 'notEqual' | 'startsWith' | 'endsWith' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'inRange' | 'before' | 'after' | 'blank' | 'notBlank';
|
|
31
|
+
export interface FilterCondition {
|
|
32
|
+
operator: FilterOperator;
|
|
33
|
+
value?: unknown;
|
|
34
|
+
/** Second bound, only used by `inRange`. */
|
|
35
|
+
valueTo?: unknown;
|
|
36
|
+
}
|
|
37
|
+
/** An AG Grid-style condition filter for one column: up to two conditions joined by AND/OR. */
|
|
38
|
+
export interface ConditionFilter {
|
|
39
|
+
conditions: FilterCondition[];
|
|
40
|
+
combinator: 'AND' | 'OR';
|
|
41
|
+
}
|
|
26
42
|
export interface DataTableProps<T> {
|
|
27
43
|
columns: ColumnConfig<T>[];
|
|
28
44
|
data: T[];
|
|
@@ -54,6 +70,7 @@ export interface DataTableProps<T> {
|
|
|
54
70
|
defaultPageSize?: number;
|
|
55
71
|
pageSizeOptions?: number[];
|
|
56
72
|
onPageChange?: (page: number, pageSize: number) => void;
|
|
73
|
+
onConditionFilterChange?: (conditionFilters: Record<string, ConditionFilter>) => void;
|
|
57
74
|
}
|
|
58
75
|
/** A group header produced by `groupBy`. */
|
|
59
76
|
export interface GroupNode<T> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ColumnConfig, SortDirection } from '../types';
|
|
1
|
+
import type { ColumnConfig, ConditionFilter, FilterCondition, FilterType, SortDirection } from '../types';
|
|
2
2
|
/** Reads a record's value for a column, honouring the column's `getValue` override. */
|
|
3
3
|
export declare const getFieldValue: <T>(record: T, key: string, column?: ColumnConfig<T>) => unknown;
|
|
4
4
|
/** Returns the row's key as a string, or `undefined` when the key is missing (callers fall back to the index). */
|
|
@@ -6,5 +6,10 @@ export declare const getRowKeyValue: <T>(record: T, rowKey: keyof T | ((record:
|
|
|
6
6
|
export declare const getRowRuleClassNames: <T>(record: T, index: number, rowClassRules?: Record<string, (record: T, index: number) => boolean>) => string | undefined;
|
|
7
7
|
export declare const getUniqueValues: <T>(data: T[], key: string, column?: ColumnConfig<T>) => unknown[];
|
|
8
8
|
export declare const filterRecords: <T>(data: T[], selectedFilters: Record<string, unknown[]>, columnsByKey: Map<string, ColumnConfig<T>>) => T[];
|
|
9
|
+
/** Infers a column's condition-filter type from its first non-null value. `column.filterType` always wins. */
|
|
10
|
+
export declare const detectFilterType: <T>(data: T[], key: string, column?: ColumnConfig<T>) => FilterType;
|
|
11
|
+
export declare const matchesCondition: (fieldValue: unknown, condition: FilterCondition, type: FilterType) => boolean;
|
|
12
|
+
/** Applies AG Grid-style condition filters (1-2 operator conditions per column, joined by AND/OR). */
|
|
13
|
+
export declare const applyConditionFilters: <T>(data: T[], conditionFilters: Record<string, ConditionFilter>, columnsByKey: Map<string, ColumnConfig<T>>) => T[];
|
|
9
14
|
/** Sorts a copy of `records`. Empty values (null/undefined/NaN) always sort last regardless of direction. */
|
|
10
15
|
export declare const sortRecords: <T>(records: T[], key: string, direction: Exclude<SortDirection, null>, column?: ColumnConfig<T>) => T[];
|
package/dist/flexi-datagrid.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
.flexi-root{width:100%;position:relative}.flexi-root>*+*{margin-top:.75rem}.flexi-settings-menu{z-index:50;background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;flex-direction:column;width:13rem;max-height:20rem;padding:.5rem;font-weight:400;display:flex;position:absolute;top:calc(100% + 4px);overflow-y:auto;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.flexi-settings-section{flex-direction:column;gap:.125rem;display:flex}.flexi-settings-heading{text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;padding:.25rem .5rem;font-size:11px;font-weight:700}.flexi-settings-divider{background-color:#f1f5f9;height:1px;margin:.375rem 0}.flexi-settings-reset{color:#ef4444;border-radius:.375rem;align-items:center;gap:.375rem;padding:.375rem .5rem;font-size:12px;font-weight:600;display:inline-flex}.flexi-settings-reset:hover{background-color:#fef2f2}.flexi-groupzone{box-sizing:border-box;background-color:#f8fafc80;border:1px dashed #e2e8f0;border-radius:.75rem;align-items:center;gap:.5rem;width:100%;min-height:52px;padding:.5rem 1rem;transition:all .2s;display:flex}.flexi-groupzone--dragover{background-color:#eff6ff80;border:1px solid #3b82f6;transform:scale(.99)}.flexi-groupzone-hint{color:#64748b;-webkit-user-select:none;user-select:none;font-size:12px;font-weight:500}.flexi-groupzone-chips{flex-wrap:wrap;align-items:center;gap:.375rem;display:flex}.flexi-groupzone-label{text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin-right:.25rem;font-size:11px;font-weight:700}.flexi-chip{color:#64748b;background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;align-items:center;gap:.375rem;padding:.25rem .25rem .25rem .5rem;font-size:12px;font-weight:600;display:flex;box-shadow:0 1px 2px #0000000d}.flexi-chip-remove{border-radius:5px;width:18px;height:18px}.flexi-table-wrap{box-sizing:border-box;background-color:#fff;border:1px solid #f1f5f9;border-radius:.75rem;width:100%;position:relative;overflow:auto;box-shadow:0 1px 2px #0000000d}.flexi-table{caption-side:bottom;border-collapse:collapse;border-spacing:0;table-layout:fixed;width:100%;font-size:.875rem;position:relative}.flexi-table--bordered th,.flexi-table--bordered td{border:1px solid #e2e8f0}.flexi-thead{-webkit-user-select:none;user-select:none;z-index:40;position:sticky}.flexi-header-row{border-bottom:1px solid #f1f5f9}.flexi-group-spacer-th{border-right:1px solid #f1f5f9;width:48px}.flexi-th{text-align:left;vertical-align:middle;z-index:25;background-color:#f8fafc;border-bottom:1px solid #f1f5f9;border-right:1px solid #f1f5f9;height:48px;padding:0 1rem;font-size:12px;font-weight:700;position:sticky;top:0}.flexi-th--draggable{cursor:grab}.flexi-th--draggable:active{cursor:grabbing}.flexi-th--pinned{background-color:#f8fafc}.flexi-th-inner{justify-content:space-between;align-items:center;gap:.25rem;width:100%;min-width:0;display:flex}.flexi-th-label{white-space:nowrap;text-overflow:ellipsis;flex:auto;align-items:center;gap:.375rem;min-width:0;display:flex;overflow:hidden}.flexi-th-label--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.flexi-th-label--sortable:hover{color:#2563eb}.flexi-th-text{white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;overflow:hidden}.flexi-sort-icon,.flexi-filter-btn,.flexi-pin-btn{color:#94a3b8;opacity:0;transition:color .15s,opacity .15s}.flexi-sort-icon--active{color:#2563eb;opacity:1}.flexi-th-inner:hover .flexi-sort-icon,.flexi-th-inner:hover .flexi-filter-btn,.flexi-th-inner:hover .flexi-pin-btn{opacity:1}.flexi-th-actions{flex:none;align-items:center;gap:2px;display:flex}.flexi-th-resizer{cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none;z-index:5;width:6px;position:absolute;top:0;bottom:0;right:0}.flexi-th-resizer:hover,.flexi-th-resizer:active{background-color:#93c5fd}.flexi-icon-btn{color:#64748b;background-color:#0000;border-radius:6px;justify-content:center;align-items:center;width:22px;height:22px;margin:0;padding:0;transition:background-color .15s,color .15s,transform .1s;display:inline-flex}.flexi-icon-btn:hover{color:#334155;background-color:#e2e8f0}.flexi-icon-btn:active{background-color:#cbd5e1;transform:scale(.92)}.flexi-icon-btn:focus-visible{outline-offset:1px;outline:2px solid #93c5fd}.flexi-filter-btn--active{color:#2563eb;opacity:1}.flexi-filter-btn--active:hover{color:#2563eb;opacity:1;background-color:#dbeafe}.flexi-pin-btn--pinned{color:#3b82f6;opacity:1}.flexi-pin-btn--pinned:hover{color:#2563eb;opacity:1;background-color:#dbeafe}.flexi-settings-corner{z-index:60;width:100%;height:0;position:sticky;top:0;left:0}.flexi-settings-corner-inner{background:#f8fafc;border-bottom:1px solid #e3e4e3;border-left:1px solid #e3e4e3;align-items:center;height:48px;padding:0 .5rem 0 1rem;display:flex;position:absolute;top:0;right:0}.flexi-filter-dropdown{color:#334155;text-align:left;pointer-events:auto;background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;flex-direction:column;width:13rem;padding:.5rem 0;font-weight:400;display:flex;position:absolute;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.flexi-filter-search{border-bottom:1px solid #f1f5f9;margin-bottom:.25rem;padding:0 .5rem .5rem}.flexi-input{color:#475569;box-sizing:border-box;border:1px solid #e2e8f0;border-radius:.25rem;width:100%;padding:.25rem .5rem;font-size:12px;font-weight:500}.flexi-input:focus{border-color:#3b82f6;outline:none}.flexi-filter-list{flex-direction:column;gap:.25rem;max-height:10rem;padding:0 .5rem;display:flex;overflow-y:auto}.flexi-filter-empty{color:#64748b;text-align:center;padding:.5rem;font-size:11px}.flexi-filter-option{cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:.25rem;align-items:center;gap:.5rem;padding:.375rem .5rem;font-size:12px;display:flex}.flexi-filter-option:hover{background-color:#f8fafc}.flexi-filter-option-text{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.flexi-checkbox{accent-color:#2563eb;border:1px solid #cbd5e1;border-radius:.25rem;width:.875rem;height:.875rem}.flexi-checkbox:focus{outline-offset:1px;outline:2px solid #3b82f6}.flexi-filter-footer{border-top:1px solid #f1f5f9;justify-content:flex-end;margin-top:.5rem;padding:.25rem .5rem 0;display:flex}.flexi-filter-clear{color:#ef4444;padding:.125rem .5rem;font-size:11px;font-weight:500}.flexi-filter-clear:hover{text-decoration:underline}.flexi-tbody{display:table-row-group}.flexi-tbody tr+tr{border-top:1px solid #f1f5f9}.flexi-empty-cell{text-align:center;color:#64748b;padding:2rem;font-size:12px;font-weight:500}.flexi-group-row{-webkit-user-select:none;user-select:none;background-color:#f8fafc99;height:48px;transition:background-color .15s}.flexi-group-row:hover{background-color:#f1f5f9cc}.flexi-group-cell{color:#334155;border-right:1px solid #f1f5f9;padding:.75rem 1rem;font-size:12px;font-weight:600}.flexi-group-toggle{text-align:left;border-radius:6px;align-items:center;gap:.375rem;margin:-4px -8px -4px -4px;padding:4px 8px 4px 4px;transition:background-color .15s;display:flex}.flexi-group-toggle:hover{background-color:#e2e8f099}.flexi-group-toggle:focus-visible{outline-offset:1px;outline:2px solid #93c5fd}.flexi-icon-muted{color:#64748b}.flexi-group-value{color:#1e293b;white-space:nowrap;text-overflow:ellipsis;width:10rem;padding:4px 10px;font-weight:700;overflow:hidden}.flexi-group-count{color:#64748b;background-color:#e2e8f0b3;border-radius:9999px;margin-left:.25rem;padding:.05rem .375rem;font-size:10px;font-weight:500}.flexi-group-filler{background-color:#f8fafc4d}.flexi-row{border-bottom:1px solid #f1f5f9;height:48px;transition:background-color .15s}.flexi-row:hover{background-color:#f8fafccc}.flexi-row--clickable{cursor:pointer}.flexi-row-indent-cell{background-color:#fff;border-right:1px solid #f1f5f9;width:48px}.flexi-cell{vertical-align:middle;padding:.25rem 1rem;transition:background-color .15s}.flexi-row:hover .flexi-cell--pinned{background-color:#f1f5f9}.flexi-cell-text{color:#64748b;white-space:nowrap;text-overflow:ellipsis;font-size:12px;font-weight:500;display:block;overflow:hidden}.flexi-actions-cell{vertical-align:middle;text-align:right;background-color:#fff;padding:.25rem 1rem;box-shadow:-1px 0 #e2e8f0}.flexi-row:hover .flexi-actions-cell{background-color:#f8fafccc}.flexi-tbody--pinned-bottom{box-shadow:0 -2px 5px -2px #0000001a}.flexi-row--pinned-bottom{background-color:#f8fafc;font-weight:600}.flexi-row--pinned-bottom:hover,.flexi-row--pinned-bottom .flexi-cell,.flexi-row--pinned-bottom .flexi-actions-cell{background-color:#f8fafc}.flexi-row--pinned-bottom .flexi-cell-text{color:#334155}.flexi-actions-inner{opacity:0;justify-content:flex-end;gap:.25rem;transition:opacity .15s;display:flex}.flexi-row:hover .flexi-actions-inner{opacity:1}.flexi-icon-xs{width:.75rem;height:.75rem}.flexi-icon-sm{width:.875rem;height:.875rem}.flexi-icon-md{width:1rem;height:1rem}.flexi-pagination{color:#475569;box-sizing:border-box;background-color:#f8fafc80;border:1px solid #e2e8f0;border-radius:.75rem;justify-content:space-between;align-items:center;gap:1rem;width:100%;padding:.5rem .75rem;font-size:12px;font-weight:500;display:flex}.flexi-pagination-size{align-items:center;gap:.5rem;display:flex}.flexi-pagination-select{color:#334155;background-color:#fff;border:1px solid #e2e8f0;border-radius:.375rem;padding:.25rem .5rem;font-size:12px;font-weight:500}.flexi-pagination-info{text-align:center;flex:auto}.flexi-pagination-controls{align-items:center;gap:.25rem;display:flex}.flexi-pagination-controls .flexi-icon-btn:disabled{opacity:.4;cursor:not-allowed}.flexi-pagination-controls .flexi-icon-btn:disabled:hover{background-color:#0000}.flexi-pagination-page{white-space:nowrap;padding:0 .5rem}.flexi-root button{appearance:none;cursor:pointer;font:inherit;color:inherit;background:0 0;border:none;margin:0;padding:0 5px;line-height:normal}.flexi-root input{font:inherit}.flexi-root table{border-collapse:collapse;border-spacing:0}
|
|
1
|
+
.flexi-root{width:100%;height:100%;position:relative}.flexi-root>*+*{margin-top:.75rem}.flexi-settings-menu{z-index:50;background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;flex-direction:column;width:13rem;max-height:20rem;padding:.5rem;font-weight:400;display:flex;position:absolute;top:calc(100% + 4px);overflow-y:auto;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.flexi-settings-section{flex-direction:column;gap:.125rem;display:flex}.flexi-settings-heading{text-transform:uppercase;letter-spacing:.05em;color:#94a3b8;padding:.25rem .5rem;font-size:11px;font-weight:700}.flexi-settings-divider{background-color:#f1f5f9;height:1px;margin:.375rem 0}.flexi-settings-reset{color:#ef4444;border-radius:.375rem;align-items:center;gap:.375rem;padding:.375rem .5rem;font-size:12px;font-weight:600;display:inline-flex}.flexi-settings-reset:hover{background-color:#fef2f2}.flexi-groupzone{box-sizing:border-box;background-color:#f8fafc80;border:1px dashed #e2e8f0;border-radius:.75rem;align-items:center;gap:.5rem;width:100%;min-height:52px;padding:.5rem 1rem;transition:all .2s;display:flex}.flexi-groupzone--dragover{background-color:#eff6ff80;border:1px solid #3b82f6;transform:scale(.99)}.flexi-groupzone-hint{color:#64748b;-webkit-user-select:none;user-select:none;font-size:12px;font-weight:500}.flexi-groupzone-chips{flex-wrap:wrap;align-items:center;gap:.375rem;display:flex}.flexi-groupzone-label{text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin-right:.25rem;font-size:11px;font-weight:700}.flexi-chip{color:#64748b;background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;align-items:center;gap:.375rem;padding:.25rem .25rem .25rem .5rem;font-size:12px;font-weight:600;display:flex;box-shadow:0 1px 2px #0000000d}.flexi-chip-remove{border-radius:5px;width:18px;height:18px}.flexi-table-wrap{box-sizing:border-box;background-color:#fff;border:1px solid #f1f5f9;border-radius:.75rem;width:100%;position:relative;overflow:auto;box-shadow:0 1px 2px #0000000d}.flexi-table{caption-side:bottom;border-collapse:collapse;border-spacing:0;table-layout:fixed;width:100%;font-size:.875rem;position:relative}.flexi-table--bordered th,.flexi-table--bordered td{border:1px solid #e2e8f0}.flexi-thead{-webkit-user-select:none;user-select:none;z-index:40;position:sticky}.flexi-header-row{border-bottom:1px solid #f1f5f9}.flexi-group-spacer-th{border-right:1px solid #f1f5f9;width:48px}.flexi-th{text-align:left;vertical-align:middle;z-index:25;background-color:#f8fafc;border-bottom:1px solid #f1f5f9;border-right:1px solid #f1f5f9;height:48px;padding:0 1rem;font-size:12px;font-weight:700;position:sticky;top:0}.flexi-th--draggable{cursor:grab}.flexi-th--draggable:active{cursor:grabbing}.flexi-th--pinned{background-color:#f8fafc}.flexi-th-inner{justify-content:space-between;align-items:center;gap:.25rem;width:100%;min-width:0;display:flex}.flexi-th-label{white-space:nowrap;text-overflow:ellipsis;flex:auto;align-items:center;gap:.375rem;min-width:0;display:flex;overflow:hidden}.flexi-th-label--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.flexi-th-label--sortable:hover{color:#2563eb}.flexi-th-text{white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;overflow:hidden}.flexi-sort-icon,.flexi-filter-btn,.flexi-pin-btn{color:#94a3b8;opacity:0;transition:color .15s,opacity .15s}.flexi-sort-icon--active{color:#2563eb;opacity:1}.flexi-th-inner:hover .flexi-sort-icon,.flexi-th-inner:hover .flexi-filter-btn,.flexi-th-inner:hover .flexi-pin-btn{opacity:1}.flexi-th-actions{flex:none;align-items:center;gap:2px;display:flex}.flexi-th-resizer{cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none;z-index:5;width:6px;position:absolute;top:0;bottom:0;right:0}.flexi-th-resizer:hover,.flexi-th-resizer:active{background-color:#93c5fd}.flexi-icon-btn{color:#64748b;background-color:#0000;border-radius:6px;justify-content:center;align-items:center;width:22px;height:22px;margin:0;padding:0;transition:background-color .15s,color .15s,transform .1s;display:inline-flex}.flexi-icon-btn:hover{color:#334155;background-color:#e2e8f0}.flexi-icon-btn:active{background-color:#cbd5e1;transform:scale(.92)}.flexi-icon-btn:focus-visible{outline-offset:1px;outline:2px solid #93c5fd}.flexi-filter-btn--active{color:#2563eb;opacity:1}.flexi-filter-btn--active:hover{color:#2563eb;opacity:1;background-color:#dbeafe}.flexi-pin-btn--pinned{color:#3b82f6;opacity:1}.flexi-pin-btn--pinned:hover{color:#2563eb;opacity:1;background-color:#dbeafe}.flexi-settings-corner{z-index:60;width:100%;height:0;position:sticky;top:0;left:0}.flexi-settings-corner-inner{background:#f8fafc;border-bottom:1px solid #e3e4e3;border-left:1px solid #e3e4e3;align-items:center;height:48px;padding:0 .5rem 0 1rem;display:flex;position:absolute;top:0;right:0}.flexi-filter-dropdown{color:#334155;text-align:left;pointer-events:auto;background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;flex-direction:column;width:15rem;padding:.5rem 0;font-weight:400;display:flex;position:absolute;box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a}.flexi-filter-search{border-bottom:1px solid #f1f5f9;margin-bottom:.25rem;padding:0 .5rem .5rem}.flexi-input{color:#475569;box-sizing:border-box;border:1px solid #e2e8f0;border-radius:.25rem;width:100%;padding:.25rem .5rem;font-size:12px;font-weight:500}.flexi-input:focus{border-color:#3b82f6;outline:none}.flexi-filter-list{flex-direction:column;gap:.25rem;max-height:10rem;padding:0 .5rem;display:flex;overflow-y:auto}.flexi-filter-empty{color:#64748b;text-align:center;padding:.5rem;font-size:11px}.flexi-filter-option{cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:.25rem;align-items:center;gap:.5rem;padding:.375rem .5rem;font-size:12px;display:flex}.flexi-filter-option:hover{background-color:#f8fafc}.flexi-filter-option-text{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.flexi-checkbox{accent-color:#2563eb;border:1px solid #cbd5e1;border-radius:.25rem;width:.875rem;height:.875rem}.flexi-checkbox:focus{outline-offset:1px;outline:2px solid #3b82f6}.flexi-filter-footer{border-top:1px solid #f1f5f9;justify-content:flex-end;margin-top:.5rem;padding:.25rem .5rem 0;display:flex}.flexi-filter-clear{color:#ef4444;padding:.125rem .5rem;font-size:11px;font-weight:500}.flexi-filter-clear:hover{text-decoration:underline}.flexi-filter-tabs{border-bottom:1px solid #f1f5f9;gap:.25rem;margin-bottom:.25rem;padding:0 .5rem .5rem;display:flex}.flexi-filter-tab{color:#64748b;text-align:center;background-color:#0000;border-radius:.25rem;flex:1;padding:.25rem .5rem;font-size:11px;font-weight:500}.flexi-filter-tab:hover{background-color:#f8fafc}.flexi-filter-tab--active{color:#2563eb;background-color:#eff6ff}.flexi-condition-filter{flex-direction:column;gap:.5rem;padding:0 .5rem;display:flex}.flexi-condition-row{flex-direction:column;gap:.375rem;display:flex}.flexi-condition-values{gap:.375rem;display:flex}.flexi-condition-values .flexi-input{flex:1;min-width:0}.flexi-condition-toggle{padding:.25rem 0}.flexi-condition-combinator{color:#475569;gap:1rem;font-size:12px;display:flex}.flexi-condition-combinator label{cursor:pointer;align-items:center;gap:.25rem;display:flex}.flexi-tbody{display:table-row-group}.flexi-tbody tr+tr{border-top:1px solid #f1f5f9}.flexi-empty-cell{text-align:center;color:#64748b;padding:2rem;font-size:12px;font-weight:500}.flexi-group-row{-webkit-user-select:none;user-select:none;background-color:#f8fafc99;height:48px;transition:background-color .15s}.flexi-group-row:hover{background-color:#f1f5f9cc}.flexi-group-cell{color:#334155;border-right:1px solid #f1f5f9;padding:.75rem 1rem;font-size:12px;font-weight:600}.flexi-group-toggle{text-align:left;border-radius:6px;align-items:center;gap:.375rem;margin:-4px -8px -4px -4px;padding:4px 8px 4px 4px;transition:background-color .15s;display:flex}.flexi-group-toggle:hover{background-color:#e2e8f099}.flexi-group-toggle:focus-visible{outline-offset:1px;outline:2px solid #93c5fd}.flexi-icon-muted{color:#64748b}.flexi-group-value{color:#1e293b;white-space:nowrap;text-overflow:ellipsis;width:10rem;padding:4px 10px;font-weight:700;overflow:hidden}.flexi-group-count{color:#64748b;background-color:#e2e8f0b3;border-radius:9999px;margin-left:.25rem;padding:.05rem .375rem;font-size:10px;font-weight:500}.flexi-group-filler{background-color:#f8fafc4d}.flexi-row{border-bottom:1px solid #f1f5f9;height:48px;transition:background-color .15s}.flexi-row:hover{background-color:#f8fafccc}.flexi-row--clickable{cursor:pointer}.flexi-row-indent-cell{background-color:#fff;border-right:1px solid #f1f5f9;width:48px}.flexi-cell{vertical-align:middle;padding:.25rem 1rem;transition:background-color .15s}.flexi-row:hover .flexi-cell--pinned{background-color:#f1f5f9}.flexi-cell-text{color:#64748b;white-space:nowrap;text-overflow:ellipsis;font-size:12px;font-weight:500;display:block;overflow:hidden}.flexi-actions-cell{vertical-align:middle;text-align:right;background-color:#fff;padding:.25rem 1rem;box-shadow:-1px 0 #e2e8f0}.flexi-row:hover .flexi-actions-cell{background-color:#f8fafccc}.flexi-tbody--pinned-bottom{box-shadow:0 -2px 5px -2px #0000001a}.flexi-row--pinned-bottom{background-color:#f8fafc;font-weight:600}.flexi-row--pinned-bottom:hover,.flexi-row--pinned-bottom .flexi-cell,.flexi-row--pinned-bottom .flexi-actions-cell{background-color:#f8fafc}.flexi-row--pinned-bottom .flexi-cell-text{color:#334155}.flexi-actions-inner{opacity:0;justify-content:flex-end;gap:.25rem;transition:opacity .15s;display:flex}.flexi-row:hover .flexi-actions-inner{opacity:1}.flexi-icon-xs{width:.75rem;height:.75rem}.flexi-icon-sm{width:.875rem;height:.875rem}.flexi-icon-md{width:1rem;height:1rem}.flexi-pagination{color:#475569;box-sizing:border-box;background-color:#f8fafc80;border:1px solid #e2e8f0;border-radius:.75rem;justify-content:space-between;align-items:center;gap:1rem;width:100%;padding:.5rem .75rem;font-size:12px;font-weight:500;display:flex}.flexi-pagination-size{align-items:center;gap:.5rem;display:flex}.flexi-pagination-select{color:#334155;background-color:#fff;border:1px solid #e2e8f0;border-radius:.375rem;padding:.25rem .5rem;font-size:12px;font-weight:500}.flexi-pagination-info{text-align:center;flex:auto}.flexi-pagination-controls{align-items:center;gap:.25rem;display:flex}.flexi-pagination-controls .flexi-icon-btn:disabled{opacity:.4;cursor:not-allowed}.flexi-pagination-controls .flexi-icon-btn:disabled:hover{background-color:#0000}.flexi-pagination-page{white-space:nowrap;padding:0 .5rem}.flexi-root button{appearance:none;cursor:pointer;font:inherit;color:inherit;background:0 0;border:none;margin:0;padding:0 5px;line-height:normal}.flexi-root input{font:inherit}.flexi-root table{border-collapse:collapse;border-spacing:0}
|
|
2
2
|
/*$vite$:1*/
|