@opencorestack/opengridx 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +10 -1
  3. package/dist/ai.cjs +2 -0
  4. package/dist/ai.cjs.map +1 -0
  5. package/dist/ai.d.ts +366 -0
  6. package/dist/ai.es.js +2 -0
  7. package/dist/ai.es.js.map +1 -0
  8. package/dist/index.d.ts +396 -6
  9. package/dist/opengridx.css +1 -1
  10. package/dist/opengridx.es.js +8029 -6090
  11. package/dist/opengridx.es.js.map +1 -1
  12. package/dist/opengridx.umd.js +16 -14
  13. package/dist/opengridx.umd.js.map +1 -1
  14. package/docs/API_REFERENCE.md +182 -4
  15. package/docs/README.md +5 -1
  16. package/docs/architecture/datagrid-orchestration.md +19 -5
  17. package/docs/components/filter-panel.md +1 -0
  18. package/docs/components/header.md +4 -1
  19. package/docs/components/toolbar.md +7 -0
  20. package/docs/customization/theming.md +12 -1
  21. package/docs/features/ai-toolkit.md +213 -0
  22. package/docs/features/cell-selection.md +8 -2
  23. package/docs/features/clipboard.md +88 -0
  24. package/docs/features/editing-reordering.md +50 -0
  25. package/docs/features/fill-handle.md +125 -0
  26. package/docs/features/filtering.md +3 -0
  27. package/docs/features/header-filters.md +138 -0
  28. package/docs/features/keyboard-navigation.md +31 -2
  29. package/docs/features/undo-redo.md +79 -0
  30. package/docs/roadmap.md +3 -4
  31. package/lib/ai/agentTools.ts +160 -0
  32. package/lib/ai/coerce.ts +90 -0
  33. package/lib/ai/columns.ts +90 -0
  34. package/lib/ai/index.ts +33 -0
  35. package/lib/ai/promptHandler.ts +109 -0
  36. package/lib/ai/schema.ts +214 -0
  37. package/lib/ai/types.ts +152 -0
  38. package/lib/ai/validate.ts +296 -0
  39. package/lib/components/AiAssistant/GridAiAssistantArea.tsx +26 -0
  40. package/lib/components/AiAssistant/GridAiAssistantPanel.css +171 -0
  41. package/lib/components/AiAssistant/GridAiAssistantPanel.tsx +145 -0
  42. package/lib/components/AiAssistant/useSpeechInput.ts +74 -0
  43. package/lib/components/Cell/Cell.css +33 -0
  44. package/lib/components/Cell/Cell.tsx +6 -2
  45. package/lib/components/Cell/GridEditInputCell.css +12 -4
  46. package/lib/components/Cell/GridEditInputCell.tsx +19 -12
  47. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +14 -0
  48. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +3 -2
  49. package/lib/components/DataGrid/DataGrid.tsx +138 -5
  50. package/lib/components/DataGrid/GridLiveRegion.tsx +5 -2
  51. package/lib/components/DataGrid/GridPinnedRows.tsx +5 -1
  52. package/lib/components/DataGrid/GridVirtualRows.tsx +5 -1
  53. package/lib/components/FilterPanel/FilterPanel.css +14 -0
  54. package/lib/components/FilterPanel/FilterPanel.tsx +5 -11
  55. package/lib/components/Header/Header.tsx +44 -23
  56. package/lib/components/Header/headerColumnStyle.ts +28 -0
  57. package/lib/components/HeaderFilter/HeaderFilter.css +160 -0
  58. package/lib/components/HeaderFilter/HeaderFilterCell.tsx +370 -0
  59. package/lib/components/HeaderFilter/HeaderFilterOperatorMenu.tsx +124 -0
  60. package/lib/components/HeaderFilter/HeaderFilterRow.tsx +157 -0
  61. package/lib/components/ListView/ListView.css +2 -4
  62. package/lib/components/ListView/ListViewRow.tsx +2 -2
  63. package/lib/components/Toolbar/GlobalSearch.css +16 -0
  64. package/lib/components/Toolbar/GlobalSearch.tsx +3 -2
  65. package/lib/components/Toolbar/GridToolbar.tsx +51 -0
  66. package/lib/components/ui/Checkbox.tsx +16 -5
  67. package/lib/components/ui/Input.css +43 -0
  68. package/lib/components/ui/Input.tsx +24 -5
  69. package/lib/hooks/core/gridApiPlaceholder.ts +16 -1
  70. package/lib/hooks/core/gridToolbarHostContext.ts +5 -0
  71. package/lib/hooks/core/useDataGrid.ts +27 -0
  72. package/lib/hooks/core/useGridApiMethods.ts +39 -0
  73. package/lib/hooks/core/useGridAriaRows.ts +5 -3
  74. package/lib/hooks/core/useGridCellSelection.ts +107 -13
  75. package/lib/hooks/core/useGridClipboardPaste.ts +283 -0
  76. package/lib/hooks/core/useGridColumnsPanel.ts +24 -1
  77. package/lib/hooks/core/useGridControlledState.ts +27 -0
  78. package/lib/hooks/core/useGridFillHandle.ts +323 -0
  79. package/lib/hooks/core/useGridKeyboardNavigation.ts +29 -10
  80. package/lib/hooks/core/useGridToolbarProps.ts +19 -0
  81. package/lib/hooks/core/useGridUndoRedo.ts +220 -0
  82. package/lib/hooks/features/useGridAiAssistant.ts +284 -0
  83. package/lib/hooks/features/useGridBatchEdit.ts +217 -0
  84. package/lib/hooks/features/useGridEditing.ts +24 -45
  85. package/lib/index.ts +10 -0
  86. package/lib/styles/opengridx.css +4 -1
  87. package/lib/theme/DataGridThemeProvider.tsx +2 -0
  88. package/lib/theme/types.ts +2 -0
  89. package/lib/types/index.ts +343 -1
  90. package/lib/utils/aiAssistant.ts +205 -0
  91. package/lib/utils/aiBrand.ts +17 -0
  92. package/lib/utils/cellSelection.ts +25 -5
  93. package/lib/utils/editing/commit.ts +69 -0
  94. package/lib/utils/editing/fill.ts +238 -0
  95. package/lib/utils/editing/targets.ts +109 -0
  96. package/lib/utils/filtering/index.ts +14 -0
  97. package/lib/utils/focus/index.ts +9 -3
  98. package/lib/utils/headerFilters.ts +189 -0
  99. package/lib/utils/parsing.ts +252 -0
  100. package/llms.txt +97 -3
  101. package/package.json +8 -2
@@ -17,6 +17,7 @@ import type {
17
17
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
18
18
  import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
19
19
  import { getRowCellRange } from '../../utils/cellSelection';
20
+ import type { GridFillHandlePosition } from '../../utils/cellSelection';
20
21
  import type { GridEditingState } from '../../hooks/features/useGridEditing';
21
22
 
22
23
  interface PinnedRowEditingHandlers {
@@ -64,6 +65,8 @@ export interface GridPinnedRowsProps<R extends GridRowModel> {
64
65
  cellRange?: GridDisplayCellRange | null;
65
66
  /** The grid has row spans: rows tell their cells how far the range bottom is. */
66
67
  cellRangeHasRowSpan?: boolean;
68
+ /** Where the fill handle is drawn (v3.5), or null. */
69
+ fillHandle?: GridFillHandlePosition | null;
67
70
  /** Pinned rows are edited like any other row. */
68
71
  editingHandlers?: PinnedRowEditingHandlers;
69
72
  isCellEditable?: (params: GridCellParams<R>) => boolean;
@@ -111,6 +114,7 @@ export function GridPinnedRows<R extends GridRowModel>({
111
114
  rowReordering = false,
112
115
  cellRange,
113
116
  cellRangeHasRowSpan = false,
117
+ fillHandle,
114
118
  }: GridPinnedRowsProps<R>) {
115
119
  if (rows.length === 0) return null;
116
120
 
@@ -156,7 +160,7 @@ export function GridPinnedRows<R extends GridRowModel>({
156
160
  onEditCellValueChange={editingHandlers?.setEditCellValue}
157
161
  isCellEditable={isCellEditable}
158
162
  rowReordering={rowReordering}
159
- cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan)}
163
+ cellRange={getRowCellRange(cellRange, rowIndex, cellRangeHasRowSpan, fillHandle)}
160
164
  />
161
165
  );
162
166
  })}
@@ -20,6 +20,7 @@ import type { GridEditingState } from '../../hooks/features/useGridEditing';
20
20
  import type { CellColSpanInfo, RowSpanningCaches } from '../../hooks/features/useGridSpanning';
21
21
  import type { GridDisplayCellRange } from '../../hooks/core/useGridCellSelection';
22
22
  import { getRowCellRange } from '../../utils/cellSelection';
23
+ import type { GridFillHandlePosition } from '../../utils/cellSelection';
23
24
 
24
25
  interface RowEditingHandlers {
25
26
  editingCell: GridEditingState['editingCell'];
@@ -77,6 +78,8 @@ export interface GridVirtualRowsProps<R extends GridRowModel> {
77
78
  cellRange?: GridDisplayCellRange | null;
78
79
  /** The grid has row spans: rows tell their cells how far the range bottom is. */
79
80
  cellRangeHasRowSpan?: boolean;
81
+ /** Where the fill handle is drawn (v3.5), or null. */
82
+ fillHandle?: GridFillHandlePosition | null;
80
83
  }
81
84
 
82
85
  export function GridVirtualRows<R extends GridRowModel>({
@@ -123,6 +126,7 @@ export function GridVirtualRows<R extends GridRowModel>({
123
126
  columnIndexMap,
124
127
  cellRange,
125
128
  cellRangeHasRowSpan = false,
129
+ fillHandle,
126
130
  }: GridVirtualRowsProps<R>) {
127
131
  const skeletonColumns: GridColDef<R>[] = baseColumns.length > 0
128
132
  ? baseColumns
@@ -214,7 +218,7 @@ export function GridVirtualRows<R extends GridRowModel>({
214
218
  rowSpanningCaches={rowSpanningCaches}
215
219
  rowHeight={rowHeight}
216
220
  rowMeta={rowMetaMap.get(id)}
217
- cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan)}
221
+ cellRange={getRowCellRange(cellRange, actualIndex, cellRangeHasRowSpan, fillHandle)}
218
222
  />
219
223
  ))
220
224
  )}
@@ -115,11 +115,25 @@
115
115
  border-radius: var(--ogx-border-radius-sm);
116
116
  font-family: var(--ogx-font-family);
117
117
  font-size: var(--ogx-font-size-xs);
118
+ line-height: revert;
118
119
  background: var(--ogx-grid-background);
119
120
  color: var(--ogx-grid-row-text);
120
121
  outline: none;
121
122
  transition: border-color 0.15s, box-shadow 0.15s;
122
123
  }
124
+ /* The text/date value box renders through the shared Input: the <input> keeps its own border,
125
+ so the wrapper draws nothing and only takes the input's place in the row. */
126
+ .ogx-filter__controls .ogx-input-wrapper {
127
+ flex: 1;
128
+ min-width: 60px;
129
+ border: none;
130
+ border-radius: 0;
131
+ background: transparent;
132
+ transition: none;
133
+ }
134
+ .ogx-filter__controls .ogx-input-wrapper:focus-within {
135
+ box-shadow: none;
136
+ }
123
137
  .ogx-filter__value-input::placeholder {
124
138
  color: var(--ogx-overlay-text, var(--ogx-color-gray-400));
125
139
  opacity: 0.6;
@@ -6,8 +6,8 @@ import type {
6
6
  GridColDef,
7
7
  GridFilterOperator
8
8
  } from '../../types';
9
- import { getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
- import { toLocalDateString } from '../../utils/values';
9
+ import { filterValueToInputText, getOperatorsForType, NO_VALUE_OPERATORS } from '../../utils/filtering';
10
+ import { Input } from '../ui/Input';
11
11
 
12
12
 
13
13
  export interface FilterPanelProps {
@@ -73,13 +73,7 @@ function matchesOption(option: ValueOption, value: unknown): boolean {
73
73
 
74
74
  /** Text shown in the value input for a committed value. Never used to emit anything. */
75
75
  function toInputText(value: unknown, col: GridColDef): string {
76
- if (value == null) return '';
77
- if (col.type === 'date' || value instanceof Date) {
78
- const day = toLocalDateString(value);
79
- return day || (typeof value === 'string' ? value : '');
80
- }
81
- if (Array.isArray(value)) return value.map(v => String(v)).join(', ');
82
- return String(value);
76
+ return filterValueToInputText(value, col.type);
83
77
  }
84
78
 
85
79
  /** The item value for typed text. isAnyOf takes a comma-separated list. */
@@ -251,10 +245,10 @@ const FilterRow: React.FC<{
251
245
  );
252
246
  } else if (showValue) {
253
247
  valueControl = (
254
- <input
248
+ <Input
255
249
  id={`${filterId}-val`}
256
250
  name={`filter-val-${col.field}`}
257
- className={valueClassName}
251
+ inputClassName={valueClassName}
258
252
  type={col.type === 'date' ? 'date' : 'text'}
259
253
  value={draft ?? toInputText(item?.value, col)}
260
254
  onChange={(e) => setDraft(e.target.value)}
@@ -2,12 +2,14 @@ import React, { useId } from 'react';
2
2
  import { Checkbox } from '../ui/Checkbox';
3
3
  import { ColumnResizeHandle } from '../ColumnResizeHandle/ColumnResizeHandle';
4
4
  import { clampResizeWidth } from '../ColumnResizeHandle/clampResizeWidth';
5
- import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel } from '../../types';
5
+ import type { GridColDef, GridRowModel, GridSortDirection, GridColumnPinning, GridAggregationModel, GridColumnGroupingModel, GridFilterModel } from '../../types';
6
6
  import { isColumnPinned, calculatePinnedPositions, getPinnedEdgeFields } from '../../utils/pinning';
7
7
  import { getRenderedColumnWidth } from '../../utils/columnWidth';
8
8
  import { buildColumnGroupRow, getColumnGroupDepth, getColumnGroupPaths } from '../../utils/columnGroups';
9
9
  import { ColumnMenu } from './ColumnMenu';
10
10
  import { getHeaderLineClamp, shouldWrapHeaderText } from '../../utils/headerWrap';
11
+ import { getHeaderColumnStyle } from './headerColumnStyle';
12
+ import { HeaderFilterRow } from '../HeaderFilter/HeaderFilterRow';
11
13
 
12
14
 
13
15
  function MenuIcon() {
@@ -60,8 +62,11 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
60
62
  onDragOver?: (field: string) => (event: React.DragEvent) => void;
61
63
  onDragEnd?: () => void;
62
64
  onDrop?: (targetField: string) => (event: React.DragEvent) => void;
63
- /** The grid's focus position; `id: null` means a header cell is focused. */
64
- focusedCell?: { id: string | number | null; field: string } | null;
65
+ /**
66
+ * The grid's focus position; `id: null` means a header cell is focused, and `headerFilter: true`
67
+ * a cell of the header filter row.
68
+ */
69
+ focusedCell?: { id: string | number | null; field: string; headerFilter?: boolean } | null;
65
70
  onHeaderClick?: (field: string) => void;
66
71
  onSortAdd?: (field: string, direction: GridSortDirection) => void;
67
72
  multiSort?: boolean;
@@ -74,6 +79,14 @@ export interface HeaderProps<R extends GridRowModel = GridRowModel> {
74
79
  wrapHeaderText?: boolean;
75
80
  /** Header row height in pixels; sets how many lines a wrapped title may take. Defaults to 56. */
76
81
  headerHeight?: number;
82
+ /** Renders the header filter row under the column headers (`DataGridProps.headerFilters`). */
83
+ headerFilters?: boolean;
84
+ /** The filter model the header filter row reads. */
85
+ filterModel?: GridFilterModel;
86
+ /** Receives the filter model after a header filter changes. */
87
+ onFilterModelChange?: (model: GridFilterModel) => void;
88
+ /** Opens the filter panel; a header filter cell showing "Custom filter" calls it. */
89
+ onOpenFilterPanel?: () => void;
77
90
  }
78
91
 
79
92
  export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps<R>) {
@@ -112,6 +125,10 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
112
125
  columnIndexMap,
113
126
  wrapHeaderText = false,
114
127
  headerHeight = 56,
128
+ headerFilters = false,
129
+ filterModel,
130
+ onFilterModelChange,
131
+ onOpenFilterPanel,
115
132
  } = props;
116
133
 
117
134
  const selectAllId = useId();
@@ -148,7 +165,7 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
148
165
  setMenuOpenParams({ colDef, anchorEl: anchor });
149
166
  };
150
167
 
151
- const isHeaderFocused = (field: string) => focusedCell?.id === null && focusedCell.field === field;
168
+ const isHeaderFocused = (field: string) => focusedCell?.id === null && !focusedCell.headerFilter && focusedCell.field === field;
152
169
 
153
170
  // aria-colindex: system columns first (reorder, expand, checkbox), then the data columns.
154
171
  const reorderColIndex = rowReordering ? 1 : 0;
@@ -437,30 +454,12 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
437
454
  colDef.headerClassName
438
455
  ].filter(Boolean).join(' ');
439
456
 
440
- const effectiveWidth = columnWidths[colDef.field] ?? colDef.width ?? 100;
441
-
442
457
  const style: React.CSSProperties = {
443
- width: effectiveWidth,
444
- minWidth: colDef.minWidth,
445
- maxWidth: colDef.maxWidth,
446
- flexGrow: colDef.flex ?? 0,
447
- flexShrink: 0,
448
- flexBasis: colDef.flex ? 'auto' : 'auto',
449
- boxSizing: 'border-box',
450
- position: isPinned ? 'sticky' : 'relative',
458
+ ...getHeaderColumnStyle(colDef, columnWidths, pinnedPosition, pinnedPosition ? pinnedPositions[colDef.field] : undefined),
451
459
  zIndex: colDef.zIndex,
452
460
  opacity: isDragging ? 0.5 : 1
453
461
  };
454
462
 
455
- if (isPinned && pinnedPosition) {
456
- const offset = pinnedPositions[colDef.field];
457
- if (pinnedPosition === 'left') {
458
- style.left = offset;
459
- } else {
460
- style.right = offset;
461
- }
462
- }
463
-
464
463
  const headerContent = colDef.renderHeader
465
464
  ? colDef.renderHeader({ field: colDef.field, colDef: colDef as unknown as GridColDef, colIndex })
466
465
  : colDef.headerName || colDef.field;
@@ -565,6 +564,28 @@ export function Header<R extends GridRowModel = GridRowModel>(props: HeaderProps
565
564
  />
566
565
  )}
567
566
  </div>{/* end .ogx__header */}
567
+
568
+ {/* ── Header filter row ── */}
569
+ {headerFilters && (
570
+ <HeaderFilterRow
571
+ columns={columns as unknown as GridColDef[]}
572
+ columnWidths={columnWidths}
573
+ pinnedColumns={pinnedColumns}
574
+ pinnedPositions={pinnedPositions}
575
+ pinnedEdges={pinnedEdges}
576
+ rowReordering={rowReordering}
577
+ hasDetailPanel={hasDetailPanel}
578
+ checkboxSelection={checkboxSelection}
579
+ pinExpandColumn={pinExpandColumn}
580
+ pinCheckboxColumn={pinCheckboxColumn}
581
+ columnIndexMap={columnIndexMap}
582
+ ariaRowIndex={groupDepth + 2}
583
+ filterModel={filterModel}
584
+ onFilterModelChange={onFilterModelChange}
585
+ onOpenFilterPanel={onOpenFilterPanel}
586
+ focusedField={focusedCell?.id === null && focusedCell.headerFilter ? focusedCell.field : null}
587
+ />
588
+ )}
568
589
  </div>
569
590
  );
570
591
  }
@@ -0,0 +1,28 @@
1
+ import type React from 'react';
2
+ import type { GridColDef, GridRowModel } from '../../types';
3
+
4
+ /**
5
+ * Width, flex and sticky offset of a data column's cell in a header row. The column header row
6
+ * and the header filter row both use it, so their cells line up whatever the pinning and widths.
7
+ */
8
+ export function getHeaderColumnStyle<R extends GridRowModel>(
9
+ colDef: GridColDef<R>,
10
+ columnWidths: Record<string, number>,
11
+ pinnedPosition: 'left' | 'right' | null,
12
+ pinnedOffset: number | undefined,
13
+ ): React.CSSProperties {
14
+ const style: React.CSSProperties = {
15
+ width: columnWidths[colDef.field] ?? colDef.width ?? 100,
16
+ minWidth: colDef.minWidth,
17
+ maxWidth: colDef.maxWidth,
18
+ flexGrow: colDef.flex ?? 0,
19
+ flexShrink: 0,
20
+ flexBasis: 'auto',
21
+ boxSizing: 'border-box',
22
+ position: pinnedPosition ? 'sticky' : 'relative',
23
+ };
24
+ if (pinnedPosition === 'left') style.left = pinnedOffset;
25
+ else if (pinnedPosition === 'right') style.right = pinnedOffset;
26
+ return style;
27
+ }
28
+
@@ -0,0 +1,160 @@
1
+ /* Header filter row (headerFilters): inside .ogx__header-wrap, under the column header row, so it
2
+ sticks with the headers. Its height (--ogx-header-filter-height) is set by the grid from
3
+ headerFilterHeight. */
4
+ .ogx__header-filter-row {
5
+ display: flex;
6
+ align-items: stretch;
7
+ height: var(--ogx-header-filter-height, 40px);
8
+ min-height: var(--ogx-header-filter-height, 40px);
9
+ background: var(--ogx-grid-header-background);
10
+ border-bottom: 1px solid var(--ogx-grid-border-color);
11
+ }
12
+ .ogx__header-filter-cell {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: 2px;
16
+ padding: 4px 6px;
17
+ box-sizing: border-box;
18
+ flex-shrink: 0;
19
+ overflow: hidden;
20
+ position: relative;
21
+ background: var(--ogx-grid-header-background);
22
+ font-family: var(--ogx-font-family);
23
+ font-size: var(--ogx-font-size-sm, 0.8125rem);
24
+ color: var(--ogx-grid-header-text);
25
+ }
26
+ .ogx__header-filter-cell:focus,
27
+ .ogx__header-filter-cell:focus-visible {
28
+ outline: none;
29
+ }
30
+ .ogx--kb .ogx__header-filter-cell--focused {
31
+ outline: 2px solid var(--ogx-grid-cell-focus-border, var(--ogx-color-primary, #2563eb));
32
+ outline-offset: -2px;
33
+ z-index: 6;
34
+ }
35
+ .ogx__header-filter-cell--pinned {
36
+ z-index: 12;
37
+ }
38
+ .ogx__header-filter-cell--pinned-left-last {
39
+ box-shadow: var(--ogx-grid-pinned-left-shadow, 4px 0 12px -4px rgba(0, 0, 0, 0.05));
40
+ }
41
+ .ogx__header-filter-cell--pinned-right-first {
42
+ margin-left: auto;
43
+ box-shadow: var(--ogx-grid-pinned-right-shadow, -4px 0 12px -4px rgba(0, 0, 0, 0.05));
44
+ }
45
+
46
+ /* Operator button: a compact symbol for the current operator; opens the operator menu. */
47
+ .ogx__header-filter-operator {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ justify-content: center;
51
+ flex-shrink: 0;
52
+ min-width: 22px;
53
+ height: 24px;
54
+ padding: 0 3px;
55
+ border: none;
56
+ border-radius: var(--ogx-border-radius-sm, 4px);
57
+ background: transparent;
58
+ color: var(--ogx-color-gray-500);
59
+ font: inherit;
60
+ font-size: 12px;
61
+ line-height: 1;
62
+ cursor: pointer;
63
+ }
64
+ .ogx__header-filter-operator:hover,
65
+ .ogx__header-filter-operator--open {
66
+ background: var(--ogx-grid-header-hover-background);
67
+ color: var(--ogx-grid-header-text);
68
+ }
69
+ .ogx__header-filter-cell--active .ogx__header-filter-operator {
70
+ color: var(--ogx-color-primary);
71
+ }
72
+
73
+ /* Value controls: the shared Input, and native selects styled to match it. */
74
+ .ogx__header-filter-input-wrapper {
75
+ flex: 1 1 auto;
76
+ min-width: 0;
77
+ height: 28px;
78
+ }
79
+ .ogx-input.ogx__header-filter-input {
80
+ width: 100%;
81
+ min-width: 0;
82
+ height: 100%;
83
+ box-sizing: border-box;
84
+ padding: 0 6px;
85
+ font-size: inherit;
86
+ }
87
+ .ogx__header-filter-select {
88
+ flex: 1 1 auto;
89
+ min-width: 0;
90
+ height: 28px;
91
+ padding: 0 4px;
92
+ box-sizing: border-box;
93
+ border: var(--ogx-border-width-thin, 1px) solid var(--ogx-border-color);
94
+ border-radius: var(--ogx-border-radius-md);
95
+ background: var(--ogx-color-white);
96
+ color: var(--ogx-color-gray-900);
97
+ font-family: var(--ogx-font-family);
98
+ font-size: inherit;
99
+ outline: none;
100
+ cursor: pointer;
101
+ }
102
+ .ogx__header-filter-select:hover {
103
+ border-color: var(--ogx-border-color-hover);
104
+ }
105
+ .ogx__header-filter-select:focus {
106
+ border-color: var(--ogx-color-primary);
107
+ box-shadow: 0 0 0 1px var(--ogx-color-primary);
108
+ }
109
+ .ogx__header-filter-cell--active .ogx-input-wrapper,
110
+ .ogx__header-filter-cell--active .ogx__header-filter-select {
111
+ border-color: var(--ogx-color-primary);
112
+ }
113
+ .ogx__header-filter-operator-label {
114
+ flex: 1 1 auto;
115
+ min-width: 0;
116
+ overflow: hidden;
117
+ text-overflow: ellipsis;
118
+ white-space: nowrap;
119
+ color: var(--ogx-color-gray-600);
120
+ font-style: italic;
121
+ }
122
+
123
+ .ogx__header-filter-clear {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ flex-shrink: 0;
128
+ width: 22px;
129
+ height: 22px;
130
+ padding: 0;
131
+ border: none;
132
+ border-radius: var(--ogx-border-radius-sm, 4px);
133
+ background: transparent;
134
+ color: var(--ogx-color-gray-500);
135
+ cursor: pointer;
136
+ }
137
+ .ogx__header-filter-clear:hover {
138
+ background: var(--ogx-grid-header-hover-background);
139
+ color: var(--ogx-grid-header-text);
140
+ }
141
+
142
+ /* Read-only label for a filter the row cannot show (OR logic, nested groups, other conditions). */
143
+ .ogx__header-filter-custom {
144
+ flex: 1 1 auto;
145
+ min-width: 0;
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ white-space: nowrap;
149
+ padding: 0 4px;
150
+ border: none;
151
+ background: transparent;
152
+ color: var(--ogx-color-primary);
153
+ font: inherit;
154
+ font-style: italic;
155
+ text-align: left;
156
+ }
157
+ button.ogx__header-filter-custom {
158
+ cursor: pointer;
159
+ text-decoration: underline dotted;
160
+ }