@drivenets/design-system 0.12.1 → 0.13.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 (81) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/index.cjs +1097 -308
  3. package/dist/index.d.cts +1118 -982
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1118 -982
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1089 -309
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +31 -31
  11. package/src/components/ds-date-input/ds-date-input.utils.ts +2 -2
  12. package/src/components/ds-drawer/ds-drawer.tsx +2 -0
  13. package/src/components/ds-form-control/ds-form-control.tsx +14 -11
  14. package/src/components/ds-form-control/ds-form-control.types.ts +4 -0
  15. package/src/components/ds-grid/ds-grid.tsx +1 -2
  16. package/src/components/ds-modal/ds-modal.tsx +3 -0
  17. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +21 -2
  18. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +14 -11
  19. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +1 -6
  20. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +2 -2
  21. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +2 -0
  22. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.tsx +42 -11
  23. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +24 -4
  24. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +22 -1
  25. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +22 -0
  26. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +1 -0
  27. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +19 -0
  28. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +2 -1
  29. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +1 -1
  30. package/src/components/ds-table/components/edit/cell-editors/cell-editors.module.scss +3 -0
  31. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +27 -0
  32. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +50 -0
  33. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +51 -0
  34. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +36 -0
  35. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +50 -0
  36. package/src/components/ds-table/components/edit/cell-editors/index.ts +14 -0
  37. package/src/components/ds-table/components/edit/cell-editors/table-edit-form-control.tsx +31 -0
  38. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +28 -0
  39. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +39 -0
  40. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.types.ts +7 -0
  41. package/src/components/ds-table/components/edit/ds-table-editable-cell/index.ts +2 -0
  42. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.module.scss +48 -0
  43. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +76 -0
  44. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.types.ts +5 -0
  45. package/src/components/ds-table/components/edit/ds-table-editing-cell/index.ts +2 -0
  46. package/src/components/ds-table/context/ds-table-context.ts +79 -1
  47. package/src/components/ds-table/ds-table-column.types.ts +12 -0
  48. package/src/components/ds-table/ds-table.module.scss +100 -10
  49. package/src/components/ds-table/ds-table.tsx +18 -19
  50. package/src/components/ds-table/ds-table.types.ts +52 -0
  51. package/src/components/ds-table/filters/adapters/create-checkbox-filter-adapter.tsx +24 -40
  52. package/src/components/ds-table/filters/adapters/create-dual-range-filter-adapter.tsx +10 -31
  53. package/src/components/ds-table/filters/adapters/create-filter-adapter.ts +6 -98
  54. package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.module.scss +36 -0
  55. package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.tsx +92 -13
  56. package/src/components/ds-table/filters/components/checkbox-filter/index.ts +1 -1
  57. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.module.scss +42 -0
  58. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.tsx +89 -0
  59. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.types.ts +25 -0
  60. package/src/components/ds-table/filters/components/column-filter-popover/index.ts +5 -0
  61. package/src/components/ds-table/filters/hooks/use-table-filters.ts +113 -78
  62. package/src/components/ds-table/filters/index.ts +2 -44
  63. package/src/components/ds-table/filters/types/column-meta.augmentation.ts +19 -0
  64. package/src/components/ds-table/filters/types/filter-adapter.types.ts +48 -81
  65. package/src/components/ds-table/hooks/use-cell-editor.ts +81 -0
  66. package/src/components/ds-table/index.ts +29 -0
  67. package/src/components/ds-table/utils/column-size.ts +9 -8
  68. package/src/components/ds-tag/ds-tag.tsx +2 -2
  69. package/src/components/ds-workspace-layout/ds-workspace-layout.context.ts +19 -0
  70. package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +166 -0
  71. package/src/components/ds-workspace-layout/ds-workspace-layout.tsx +135 -0
  72. package/src/components/ds-workspace-layout/ds-workspace-layout.types.ts +52 -0
  73. package/src/components/ds-workspace-layout/index.ts +11 -0
  74. package/src/index.ts +1 -1
  75. package/src/styles/_scrollbars.scss +37 -0
  76. package/src/utils/restore-document-body-modal-styles.ts +44 -0
  77. package/src/utils/use-ark-dialog-body-lock-cleanup.ts +36 -0
  78. package/src/components/ds-workspace/ds-workspace.module.scss +0 -55
  79. package/src/components/ds-workspace/ds-workspace.tsx +0 -37
  80. package/src/components/ds-workspace/ds-workspace.types.ts +0 -17
  81. package/src/components/ds-workspace/index.ts +0 -8
@@ -1,5 +1,9 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import styles from './checkbox-filter.module.scss';
2
4
  import { DsCheckbox } from '../../../../ds-checkbox';
5
+ import { DsTextInput } from '../../../../ds-text-input';
6
+ import { DsIcon } from '../../../../ds-icon';
3
7
 
4
8
  export interface CheckboxFilterItem<TValue = string> {
5
9
  value: TValue;
@@ -7,11 +11,30 @@ export interface CheckboxFilterItem<TValue = string> {
7
11
  metadata?: Record<string, unknown>;
8
12
  }
9
13
 
14
+ export interface CheckboxFilterLocale {
15
+ /** @default 'All' */
16
+ allLabel?: string;
17
+ /** @default 'Search' */
18
+ searchPlaceholder?: string;
19
+ /** @default 'No matches' */
20
+ noMatchesLabel?: string;
21
+ }
22
+
10
23
  export interface CheckboxFilterProps<TValue> {
11
24
  items: CheckboxFilterItem<TValue>[];
25
+ /** Custom row renderer; defaults to `item.label`. */
12
26
  renderer?: (item: CheckboxFilterItem<TValue>) => ReactNode;
13
27
  selectedItems: CheckboxFilterItem<TValue>[];
14
28
  onSelectionChange: (selectedItems: CheckboxFilterItem<TValue>[]) => void;
29
+ /** Show a case-insensitive substring search above the list. @default false */
30
+ searchable?: boolean;
31
+ /**
32
+ * Show an "All" toggle. Checked when every visible item is selected,
33
+ * indeterminate when some are, unchecked otherwise. @default false
34
+ */
35
+ selectAll?: boolean;
36
+ locale?: CheckboxFilterLocale;
37
+ className?: string;
15
38
  }
16
39
 
17
40
  export const CheckboxFilter = <TValue,>({
@@ -19,7 +42,19 @@ export const CheckboxFilter = <TValue,>({
19
42
  renderer,
20
43
  selectedItems,
21
44
  onSelectionChange,
45
+ searchable = false,
46
+ selectAll = false,
47
+ locale,
48
+ className,
22
49
  }: CheckboxFilterProps<TValue>) => {
50
+ const { allLabel = 'All', searchPlaceholder = 'Search', noMatchesLabel = 'No matches' } = locale ?? {};
51
+
52
+ const [query, setQuery] = useState('');
53
+
54
+ const visibleItems = searchable
55
+ ? items.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase()))
56
+ : items;
57
+
23
58
  const handleCheckedChange = (selected: CheckboxFilterItem<TValue>, checked: boolean) => {
24
59
  if (checked) {
25
60
  onSelectionChange([...selectedItems, selected]);
@@ -28,16 +63,60 @@ export const CheckboxFilter = <TValue,>({
28
63
  }
29
64
  };
30
65
 
31
- return items.map((item) => {
32
- const label = renderer ? renderer(item) : item.label;
33
- const checked = selectedItems.findIndex((selectedItem) => selectedItem.value === item.value) > -1;
34
- return (
35
- <DsCheckbox
36
- key={JSON.stringify(item.value)}
37
- label={label}
38
- checked={checked}
39
- onCheckedChange={(newState) => handleCheckedChange(item, newState === true)}
40
- />
41
- );
42
- });
66
+ const visibleSelectedCount = visibleItems.filter((item) =>
67
+ selectedItems.some((selected) => selected.value === item.value),
68
+ ).length;
69
+
70
+ const allVisibleSelected = visibleItems.length > 0 && visibleSelectedCount === visibleItems.length;
71
+ const someVisibleSelected = visibleSelectedCount > 0 && !allVisibleSelected;
72
+
73
+ const handleSelectAllChange = (next: boolean | 'indeterminate') => {
74
+ const shouldSelect = next === true;
75
+ const visibleValues = new Set(visibleItems.map((item) => item.value));
76
+ const withoutVisible = selectedItems.filter((item) => !visibleValues.has(item.value));
77
+ onSelectionChange(shouldSelect ? [...withoutVisible, ...visibleItems] : withoutVisible);
78
+ };
79
+
80
+ return (
81
+ <div className={classNames(styles.root, className)}>
82
+ {searchable && (
83
+ <DsTextInput
84
+ value={query}
85
+ placeholder={searchPlaceholder}
86
+ onValueChange={setQuery}
87
+ slots={{
88
+ startAdornment: <DsIcon icon="search" size="tiny" />,
89
+ }}
90
+ />
91
+ )}
92
+
93
+ <div className={styles.list} role="group">
94
+ {selectAll && visibleItems.length > 0 && (
95
+ <DsCheckbox
96
+ className={styles.checkboxItem}
97
+ label={allLabel}
98
+ checked={allVisibleSelected ? true : someVisibleSelected ? 'indeterminate' : false}
99
+ onCheckedChange={handleSelectAllChange}
100
+ />
101
+ )}
102
+
103
+ {visibleItems.length === 0 && <div className={styles.empty}>{noMatchesLabel}</div>}
104
+
105
+ {visibleItems.map((item) => {
106
+ const label = renderer ? renderer(item) : item.label;
107
+ const checked = selectedItems.some((selected) => selected.value === item.value);
108
+
109
+ return (
110
+ <DsCheckbox
111
+ key={JSON.stringify(item.value)}
112
+ className={styles.checkboxItem}
113
+ label={label}
114
+ checked={checked}
115
+ onCheckedChange={(next) => handleCheckedChange(item, next === true)}
116
+ />
117
+ );
118
+ })}
119
+ </div>
120
+ </div>
121
+ );
43
122
  };
@@ -1,2 +1,2 @@
1
1
  export { CheckboxFilter } from './checkbox-filter';
2
- export type { CheckboxFilterProps, CheckboxFilterItem } from './checkbox-filter';
2
+ export type { CheckboxFilterProps, CheckboxFilterItem, CheckboxFilterLocale } from './checkbox-filter';
@@ -0,0 +1,42 @@
1
+ .trigger {
2
+ min-width: var(--icon-width-tiny);
3
+ padding: 0;
4
+ opacity: 0;
5
+ transition: opacity 0.2s;
6
+ }
7
+
8
+ .triggerActive,
9
+ .triggerOpen {
10
+ opacity: 1;
11
+ color: var(--icon-information-main, var(--color-dap-primary-500));
12
+ }
13
+
14
+ .content {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: var(--xs);
18
+ min-width: 280px;
19
+ max-width: 320px;
20
+ padding: var(--standard);
21
+ background: var(--background);
22
+ border: 1px solid var(--border-secondary);
23
+ border-radius: 4px;
24
+ box-shadow: 0 5px 15px 0 rgba(0, 0, 0, 0.15);
25
+ z-index: 1000;
26
+
27
+ &[hidden] {
28
+ display: none;
29
+ }
30
+ }
31
+
32
+ .body {
33
+ display: flex;
34
+ flex-direction: column;
35
+ }
36
+
37
+ .footer {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: flex-end;
41
+ gap: var(--xs);
42
+ }
@@ -0,0 +1,89 @@
1
+ import { type MouseEvent, useState } from 'react';
2
+ import { Popover } from '@ark-ui/react/popover';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import classNames from 'classnames';
5
+ import styles from './column-filter-popover.module.scss';
6
+ import type { DsTableColumnFilterPopoverProps } from './column-filter-popover.types';
7
+ import { DsIcon } from '../../../../ds-icon';
8
+ import { DsButtonV3 } from '../../../../ds-button-v3';
9
+
10
+ // React portals bubble events through the React tree, so clicks inside the
11
+ // popover would otherwise reach the parent `<TableHead onClick={sortHandler}>`
12
+ // and toggle column sorting when applying or clearing the filter.
13
+ const stopPropagation = (event: MouseEvent) => event.stopPropagation();
14
+
15
+ /**
16
+ * Per-column filter popover anchored to a header cell: funnel trigger,
17
+ * adapter UI as the body, Clear / Save footer wired to `useTableFilters`.
18
+ */
19
+ export const DsTableColumnFilterPopover = ({
20
+ children,
21
+ hasActiveFilter,
22
+ onApply,
23
+ onClear,
24
+ onCancel,
25
+ locale,
26
+ className,
27
+ }: DsTableColumnFilterPopoverProps) => {
28
+ const { triggerAriaLabel = 'Filter column', clearLabel = 'Clear', saveLabel = 'Save' } = locale ?? {};
29
+
30
+ const [open, setOpen] = useState(false);
31
+
32
+ const handleOpenChange = (details: { open: boolean }) => {
33
+ if (open && !details.open) {
34
+ onCancel();
35
+ }
36
+ setOpen(details.open);
37
+ };
38
+
39
+ const handleSave = () => {
40
+ onApply();
41
+ setOpen(false);
42
+ };
43
+
44
+ const handleClear = () => {
45
+ onClear();
46
+ setOpen(false);
47
+ };
48
+
49
+ return (
50
+ <Popover.Root
51
+ open={open}
52
+ onOpenChange={handleOpenChange}
53
+ positioning={{ placement: 'bottom-end', gutter: 4 }}
54
+ >
55
+ <Popover.Trigger asChild>
56
+ <DsButtonV3
57
+ variant="tertiary"
58
+ size="tiny"
59
+ className={classNames(
60
+ styles.trigger,
61
+ hasActiveFilter && styles.triggerActive,
62
+ open && styles.triggerOpen,
63
+ )}
64
+ aria-label={triggerAriaLabel}
65
+ data-active={hasActiveFilter ? 'true' : undefined}
66
+ onClick={(event) => event.stopPropagation()}
67
+ >
68
+ <DsIcon icon="filter_alt" variant="outlined" filled={hasActiveFilter} size="tiny" />
69
+ </DsButtonV3>
70
+ </Popover.Trigger>
71
+
72
+ <Portal>
73
+ <Popover.Positioner>
74
+ <Popover.Content className={classNames(styles.content, className)} onClick={stopPropagation}>
75
+ <div className={styles.body}>{children}</div>
76
+ <div className={styles.footer}>
77
+ <DsButtonV3 variant="secondary" size="small" onClick={handleClear}>
78
+ {clearLabel}
79
+ </DsButtonV3>
80
+ <DsButtonV3 variant="primary" size="small" onClick={handleSave}>
81
+ {saveLabel}
82
+ </DsButtonV3>
83
+ </div>
84
+ </Popover.Content>
85
+ </Popover.Positioner>
86
+ </Portal>
87
+ </Popover.Root>
88
+ );
89
+ };
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface ColumnFilterPopoverLocale {
4
+ /** @default 'Filter column' */
5
+ triggerAriaLabel?: string;
6
+ /** @default 'Clear' */
7
+ clearLabel?: string;
8
+ /** @default 'Save' */
9
+ saveLabel?: string;
10
+ }
11
+
12
+ export interface DsTableColumnFilterPopoverProps {
13
+ /** Body rendered above the Clear / Save footer. */
14
+ children: ReactNode;
15
+ /** Whether the column has an applied filter; drives the trigger's active state. */
16
+ hasActiveFilter: boolean;
17
+ /** Commit the draft for this column. */
18
+ onApply: () => void;
19
+ /** Clear the draft and the applied value for this column. */
20
+ onClear: () => void;
21
+ /** Discard the draft when the popover closes without Save. */
22
+ onCancel: () => void;
23
+ locale?: ColumnFilterPopoverLocale;
24
+ className?: string;
25
+ }
@@ -0,0 +1,5 @@
1
+ export { DsTableColumnFilterPopover } from './column-filter-popover';
2
+ export type {
3
+ DsTableColumnFilterPopoverProps,
4
+ ColumnFilterPopoverLocale,
5
+ } from './column-filter-popover.types';
@@ -1,107 +1,77 @@
1
1
  import { type ReactNode, useState } from 'react';
2
2
  import type { CellContext, ColumnDef } from '@tanstack/react-table';
3
- import type { ChipItem } from '../../../ds-chip-group';
3
+ import type { TagFilterItem } from '../../../ds-tag-filter';
4
4
  import type {
5
5
  AnyAdapter,
6
+ ColumnFilterMeta,
6
7
  ColumnFilterState,
7
8
  FilterAdapter,
8
9
  FilterNavItem,
9
10
  FilterState,
11
+ ResolvedColumnFilter,
10
12
  } from '../types/filter-adapter.types';
11
13
 
14
+ const resolveColumnAdapterId = (
15
+ meta: ColumnFilterMeta | ResolvedColumnFilter | undefined,
16
+ ): string | undefined => {
17
+ if (!meta) {
18
+ return undefined;
19
+ }
20
+ if ('render' in meta) {
21
+ return meta.filterId;
22
+ }
23
+ if (meta.adapter) {
24
+ return meta.adapter.id;
25
+ }
26
+ return meta.adapterId;
27
+ };
28
+
12
29
  export interface UseTableFiltersOptions<TData, TValue, TCellValue> {
13
- /**
14
- * Array of filter adapters that define filter behavior
15
- */
16
30
  filterAdapters: FilterAdapter<TData, TValue, TCellValue>[] | AnyAdapter[];
17
-
18
- /**
19
- * Base column definitions
20
- */
21
31
  baseColumns?: ColumnDef<TData>[];
22
-
23
- /**
24
- * Controlled mode: External applied filters (source of truth).
25
- * When provided with onFiltersChange, the hook operates in controlled mode.
26
- * Keys must match adapter IDs.
27
- */
32
+ /** Controlled applied filters, keyed by adapter id. Pair with `onFiltersChange`. */
28
33
  appliedFilters?: FilterState<TValue>;
29
-
30
- /**
31
- * Controlled mode: Callback when filters change.
32
- * Called by applyFilters(), deleteChip(), clearAll().
33
- */
34
+ /** Fires from `applyFilters`, `deleteChip`, and `clearAll` in controlled mode. */
34
35
  onFiltersChange?: (filters: FilterState<TValue>) => void;
35
36
  }
36
37
 
37
38
  export interface UseTableFiltersResult<TData, TValue> {
38
- /**
39
- * Current draft filter state (uncommitted user selections)
40
- */
39
+ /** Draft filter state (uncommitted). */
41
40
  filterState: FilterState<TValue>;
42
-
43
- /**
44
- * Column filters (derived from applied state)
45
- */
41
+ /** Applied filters mapped to TanStack's column-filters shape. */
46
42
  columnFilters: ColumnFilterState<TValue>[];
47
-
48
- /**
49
- * Filter chips for display (derived from applied state)
50
- */
51
- filterChips: ChipItem[];
52
-
53
- /**
54
- * Filter navigation items with active counts
55
- */
43
+ /** Chips for `DsTagFilter`, derived from applied state. */
44
+ filterChips: TagFilterItem[];
45
+ /** Filter-nav items with active counts. */
56
46
  filterNavItems: FilterNavItem[];
57
-
58
- /**
59
- * Enhanced column definitions with filter functions
60
- */
47
+ /** `baseColumns` with `filterFn`, cell renderer and per-column meta wired in. */
61
48
  enhancedColumns: ColumnDef<TData>[];
62
-
63
- /**
64
- * Handlers for filter operations
65
- */
66
49
  handlers: {
67
- /** Update a specific filter's draft value */
50
+ /** Update a filter's draft value. */
68
51
  updateFilter: (filterId: string, value: TValue) => void;
69
- /** Commit draft state to applied state */
52
+ /** Commit all drafts to applied state. */
70
53
  applyFilters: () => void;
71
- /** Clear all filters */
54
+ /** Clear all filters. */
72
55
  clearAll: () => void;
73
- /** Delete a specific filter chip */
74
- deleteChip: (chip: ChipItem) => void;
56
+ /** Remove a chip's effect from applied state. */
57
+ deleteChip: (chip: TagFilterItem) => void;
58
+ /** Commit one column's draft (per-column popover Save). */
59
+ applyColumnFilter: (filterId: string) => void;
60
+ /** Discard one column's draft without committing. */
61
+ cancelColumnFilter: (filterId: string) => void;
62
+ /** Clear one applied filter (per-column popover Clear). */
63
+ clearColumnFilter: (filterId: string) => void;
75
64
  };
76
-
77
- /**
78
- * Render function for filter content
79
- */
65
+ /** Renders the adapter UI for a column id, or `null` if no match. */
66
+ renderColumnFilter: (filterId: string) => ReactNode;
67
+ /** Renders the adapter UI for a `FilterNavItem`. */
80
68
  renderFilterContent: (selectedFilter: FilterNavItem) => ReactNode;
81
69
  }
82
70
 
83
71
  /**
84
- * Hook to orchestrate table filtering with adapters.
85
- * Manages filter state, generates chips, and handles column definitions.
86
- *
87
- * **Uncontrolled mode** (default): Filters are managed internally.
88
- * **Controlled mode**: Filters are managed externally via appliedFilters/onFiltersChange.
89
- *
90
- * @example
91
- * // Uncontrolled mode
92
- * const { filterChips, handlers } = useTableFilters({
93
- * filterAdapters: adapters,
94
- * baseColumns: columns,
95
- * });
96
- *
97
- * @example
98
- * // Controlled mode (e.g., URL-driven filtering)
99
- * const { filterChips, handlers } = useTableFilters({
100
- * filterAdapters: adapters,
101
- * baseColumns: columns,
102
- * appliedFilters: filtersFromUrl,
103
- * onFiltersChange: (filters) => updateUrlAndRefetch(filters),
104
- * });
72
+ * Orchestrates table filtering: drafts → applied state, chips, nav counts, and
73
+ * column wiring. Uncontrolled by default; pass `appliedFilters` +
74
+ * `onFiltersChange` for URL-driven / external state.
105
75
  */
106
76
  export function useTableFilters<TData, TValue, TCellValue>({
107
77
  filterAdapters,
@@ -109,7 +79,6 @@ export function useTableFilters<TData, TValue, TCellValue>({
109
79
  appliedFilters: externalAppliedFilters,
110
80
  onFiltersChange: setExternalAppliedFilters,
111
81
  }: UseTableFiltersOptions<TData, TValue, TCellValue>): UseTableFiltersResult<TData, TValue> {
112
- // Internal variable just to avoid assignment issues of `any`.
113
82
  const _filterAdapters = filterAdapters as FilterAdapter<TData, TValue, TCellValue>[];
114
83
 
115
84
  const isControlled = externalAppliedFilters !== undefined && setExternalAppliedFilters !== undefined;
@@ -148,18 +117,41 @@ export function useTableFilters<TData, TValue, TCellValue>({
148
117
  const enhancedColumns: ColumnDef<TData>[] = !baseColumns
149
118
  ? []
150
119
  : baseColumns.map((col) => {
151
- const adapter = _filterAdapters.find((a) => a.id === col.id);
120
+ const metaFilter = col.meta?.filter as ColumnFilterMeta | undefined;
121
+ const adapterIdFromMeta = resolveColumnAdapterId(metaFilter);
122
+ const adapter =
123
+ (adapterIdFromMeta && _filterAdapters.find((a) => a.id === adapterIdFromMeta)) ||
124
+ _filterAdapters.find((a) => a.id === col.id);
125
+
152
126
  if (!adapter) {
153
127
  return col;
154
128
  }
155
129
 
156
130
  const cellRenderer = adapter.cellRenderer;
131
+ const appliedValue = appliedFilters[adapter.id];
132
+ const resolvedFilter: ResolvedColumnFilter | undefined = metaFilter
133
+ ? {
134
+ filterId: adapter.id,
135
+ hasActiveFilter: appliedValue !== undefined && adapter.getActiveFiltersCount(appliedValue) > 0,
136
+ render: () => renderForId(adapter.id),
137
+ onApply: () => applyColumnFilter(adapter.id),
138
+ onClear: () => clearColumnFilter(adapter.id),
139
+ onCancel: () => cancelColumnFilter(adapter.id),
140
+ }
141
+ : undefined;
142
+
157
143
  return {
158
144
  ...col,
159
145
  filterFn: adapter.columnFilterFn,
160
146
  ...(cellRenderer && {
161
147
  cell: (info: CellContext<TData, TCellValue>) => cellRenderer(info.getValue()),
162
148
  }),
149
+ ...(resolvedFilter && {
150
+ meta: {
151
+ ...col.meta,
152
+ filter: resolvedFilter,
153
+ },
154
+ }),
163
155
  };
164
156
  });
165
157
 
@@ -185,7 +177,43 @@ export function useTableFilters<TData, TValue, TCellValue>({
185
177
  setAppliedFilters({});
186
178
  };
187
179
 
188
- const deleteChip = (chip: ChipItem) => {
180
+ const omitKey = (source: FilterState<TValue>, filterId: string): FilterState<TValue> =>
181
+ Object.fromEntries(Object.entries(source).filter(([key]) => key !== filterId));
182
+
183
+ const applyColumnFilter = (filterId: string) => {
184
+ const adapter = _filterAdapters.find((a) => a.id === filterId);
185
+ if (!adapter) {
186
+ return;
187
+ }
188
+
189
+ const draftValue = draftFilters[filterId] as TValue;
190
+ const hasActive = adapter.getActiveFiltersCount(draftValue) > 0;
191
+ const next = hasActive
192
+ ? { ...appliedFilters, [filterId]: draftValue }
193
+ : omitKey(appliedFilters, filterId);
194
+
195
+ setAppliedFilters(next);
196
+ setPendingFilters((prev) => omitKey(prev, filterId));
197
+ };
198
+
199
+ const cancelColumnFilter = (filterId: string) => {
200
+ setPendingFilters((prev) => omitKey(prev, filterId));
201
+ };
202
+
203
+ const clearColumnFilter = (filterId: string) => {
204
+ const adapter = _filterAdapters.find((a) => a.id === filterId);
205
+ if (!adapter) {
206
+ return;
207
+ }
208
+
209
+ setPendingFilters((prev) => ({ ...omitKey(prev, filterId), [filterId]: adapter.initialValue }));
210
+
211
+ if (appliedFilters[filterId] !== undefined) {
212
+ setAppliedFilters(omitKey(appliedFilters, filterId));
213
+ }
214
+ };
215
+
216
+ const deleteChip = (chip: TagFilterItem) => {
189
217
  const filterKey = typeof chip.metadata?.key === 'string' ? chip.metadata.key : undefined;
190
218
  if (!filterKey) {
191
219
  return;
@@ -211,8 +239,8 @@ export function useTableFilters<TData, TValue, TCellValue>({
211
239
  setAppliedFilters(newFilters);
212
240
  };
213
241
 
214
- const renderFilterContent = (selectedFilter: FilterNavItem) => {
215
- const adapter = _filterAdapters.find((a) => a.id === selectedFilter.id);
242
+ const renderForId = (filterId: string): ReactNode => {
243
+ const adapter = _filterAdapters.find((a) => a.id === filterId);
216
244
  if (!adapter) {
217
245
  return null;
218
246
  }
@@ -223,6 +251,9 @@ export function useTableFilters<TData, TValue, TCellValue>({
223
251
  return adapter.renderFilter(value, onChange);
224
252
  };
225
253
 
254
+ const renderFilterContent = (selectedFilter: FilterNavItem) => renderForId(selectedFilter.id);
255
+ const renderColumnFilter = (filterId: string) => renderForId(filterId);
256
+
226
257
  return {
227
258
  filterState: draftFilters,
228
259
  columnFilters,
@@ -234,7 +265,11 @@ export function useTableFilters<TData, TValue, TCellValue>({
234
265
  applyFilters,
235
266
  clearAll,
236
267
  deleteChip,
268
+ applyColumnFilter,
269
+ cancelColumnFilter,
270
+ clearColumnFilter,
237
271
  },
238
272
  renderFilterContent,
273
+ renderColumnFilter,
239
274
  };
240
275
  }
@@ -1,50 +1,8 @@
1
- /**
2
- * Table Filters - Filter Adapter Pattern
3
- *
4
- * A plug-and-play filter system that eliminates boilerplate and centralizes filter logic.
5
- *
6
- * ## Quick Start:
7
- *
8
- * ```typescript
9
- * // 1. Create filter adapters in a config file
10
- * import { createCheckboxFilterAdapter, createDualRangeFilterAdapter } from '../filters';
11
- *
12
- * export const statusFilter = createCheckboxFilterAdapter({
13
- * id: 'status',
14
- * label: 'Status',
15
- * items: [{ value: 'active', label: 'Active' }],
16
- * });
17
- *
18
- * export const myFilters = [statusFilter];
19
- *
20
- * // 2. Use in component with useTableFilters hook
21
- * import { useTableFilters } from '../filters';
22
- *
23
- * const { columnFilters, filterChips, enhancedColumns, handlers } =
24
- * useTableFilters(myFilters, columns);
25
- * ```
26
- *
27
- * ## Available Exports:
28
- *
29
- * ### Factory Functions:
30
- * - `createCheckboxFilterAdapter` - Multi-select checkbox filters
31
- * - `createDualRangeFilterAdapter` - Numeric range filters (with multiple fields)
32
- * - `createFilterAdapter` - Base function for custom filters (full control)
33
- *
34
- * ### Hook:
35
- * - `useTableFilters` - Orchestrates multiple filters, manages state
36
- *
37
- * ### Types:
38
- * - `FilterAdapter` - Core adapter interface
39
- * - `CheckboxFilterItem` - Item type for checkbox filters
40
- * - `RangeFilterValue` - Value type for range filters
41
- *
42
- * ## Example:
43
- * See `stories/filters-panel.stories.tsx` and `stories/filters-panel/workflow-filters.config.tsx`
44
- */
1
+ import './types/column-meta.augmentation';
45
2
 
46
3
  export * from './adapters';
47
4
  export * from './types/filter-adapter.types';
48
5
  export * from './hooks';
49
6
  export type { CheckboxFilterItem, CheckboxFilterProps } from './components/checkbox-filter';
50
7
  export type { RangeFilterValue, RangeFilterProps } from './components/range-filter';
8
+ export * from './components/column-filter-popover';
@@ -0,0 +1,19 @@
1
+ import '@tanstack/react-table';
2
+ import type { ColumnFilterMeta, ResolvedColumnFilter } from './filter-adapter.types';
3
+
4
+ /**
5
+ * Adds `column.meta.filter`. Consumers set `ColumnFilterMeta`;
6
+ * `useTableFilters` swaps it for `ResolvedColumnFilter` on `enhancedColumns`
7
+ * so the header can render the trigger + popover. The header narrows the
8
+ * union by checking for `render`.
9
+ */
10
+ declare module '@tanstack/react-table' {
11
+ // `TData` / `TValue` must mirror the original `ColumnMeta` signature for
12
+ // the augmentation to merge.
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ interface ColumnMeta<TData, TValue> {
15
+ filter?: ColumnFilterMeta | ResolvedColumnFilter;
16
+ }
17
+ }
18
+
19
+ export {};