@drivenets/design-system 0.12.0 → 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 (88) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/index.cjs +1652 -455
  3. package/dist/index.d.cts +1226 -989
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1226 -989
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1643 -456
  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-catalog-layout/catalog-layout-empty-illustration.tsx +57 -0
  12. package/src/components/ds-catalog-layout/ds-catalog-layout.context.ts +19 -0
  13. package/src/components/ds-catalog-layout/ds-catalog-layout.module.scss +167 -0
  14. package/src/components/ds-catalog-layout/ds-catalog-layout.tsx +130 -0
  15. package/src/components/ds-catalog-layout/ds-catalog-layout.types.ts +62 -0
  16. package/src/components/ds-catalog-layout/index.ts +9 -0
  17. package/src/components/ds-date-input/ds-date-input.utils.ts +2 -2
  18. package/src/components/ds-drawer/ds-drawer.tsx +2 -0
  19. package/src/components/ds-form-control/ds-form-control.tsx +14 -11
  20. package/src/components/ds-form-control/ds-form-control.types.ts +4 -0
  21. package/src/components/ds-grid/ds-grid.tsx +1 -2
  22. package/src/components/ds-modal/ds-modal.tsx +3 -0
  23. package/src/components/ds-smart-tabs/ds-smart-tabs.module.scss +1 -0
  24. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +21 -2
  25. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +14 -11
  26. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +1 -6
  27. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +2 -2
  28. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +2 -0
  29. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.tsx +42 -11
  30. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +24 -4
  31. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +22 -1
  32. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +22 -0
  33. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +1 -0
  34. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +19 -0
  35. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +2 -1
  36. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +1 -1
  37. package/src/components/ds-table/components/edit/cell-editors/cell-editors.module.scss +3 -0
  38. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +27 -0
  39. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +50 -0
  40. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +51 -0
  41. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +36 -0
  42. package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +50 -0
  43. package/src/components/ds-table/components/edit/cell-editors/index.ts +14 -0
  44. package/src/components/ds-table/components/edit/cell-editors/table-edit-form-control.tsx +31 -0
  45. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +28 -0
  46. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +39 -0
  47. package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.types.ts +7 -0
  48. package/src/components/ds-table/components/edit/ds-table-editable-cell/index.ts +2 -0
  49. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.module.scss +48 -0
  50. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +76 -0
  51. package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.types.ts +5 -0
  52. package/src/components/ds-table/components/edit/ds-table-editing-cell/index.ts +2 -0
  53. package/src/components/ds-table/context/ds-table-context.ts +79 -1
  54. package/src/components/ds-table/ds-table-column.types.ts +12 -0
  55. package/src/components/ds-table/ds-table.module.scss +100 -10
  56. package/src/components/ds-table/ds-table.tsx +18 -19
  57. package/src/components/ds-table/ds-table.types.ts +52 -0
  58. package/src/components/ds-table/filters/adapters/create-checkbox-filter-adapter.tsx +24 -40
  59. package/src/components/ds-table/filters/adapters/create-dual-range-filter-adapter.tsx +10 -31
  60. package/src/components/ds-table/filters/adapters/create-filter-adapter.ts +6 -98
  61. package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.module.scss +36 -0
  62. package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.tsx +92 -13
  63. package/src/components/ds-table/filters/components/checkbox-filter/index.ts +1 -1
  64. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.module.scss +42 -0
  65. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.tsx +89 -0
  66. package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.types.ts +25 -0
  67. package/src/components/ds-table/filters/components/column-filter-popover/index.ts +5 -0
  68. package/src/components/ds-table/filters/hooks/use-table-filters.ts +113 -78
  69. package/src/components/ds-table/filters/index.ts +2 -44
  70. package/src/components/ds-table/filters/types/column-meta.augmentation.ts +19 -0
  71. package/src/components/ds-table/filters/types/filter-adapter.types.ts +48 -81
  72. package/src/components/ds-table/hooks/use-cell-editor.ts +81 -0
  73. package/src/components/ds-table/index.ts +29 -0
  74. package/src/components/ds-table/utils/column-size.ts +9 -8
  75. package/src/components/ds-tag/ds-tag.tsx +2 -2
  76. package/src/components/ds-workspace-layout/ds-workspace-layout.context.ts +19 -0
  77. package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +166 -0
  78. package/src/components/ds-workspace-layout/ds-workspace-layout.tsx +135 -0
  79. package/src/components/ds-workspace-layout/ds-workspace-layout.types.ts +52 -0
  80. package/src/components/ds-workspace-layout/index.ts +11 -0
  81. package/src/index.ts +2 -1
  82. package/src/styles/_scrollbars.scss +37 -0
  83. package/src/utils/restore-document-body-modal-styles.ts +44 -0
  84. package/src/utils/use-ark-dialog-body-lock-cleanup.ts +36 -0
  85. package/src/components/ds-workspace/ds-workspace.module.scss +0 -55
  86. package/src/components/ds-workspace/ds-workspace.tsx +0 -37
  87. package/src/components/ds-workspace/ds-workspace.types.ts +0 -17
  88. package/src/components/ds-workspace/index.ts +0 -8
@@ -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 {};
@@ -1,117 +1,84 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { Row } from '@tanstack/react-table';
3
- import type { ChipItem } from '../../../ds-chip-group';
3
+ import type { TagFilterItem } from '../../../ds-tag-filter';
4
4
 
5
5
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
6
  export type AnyAdapter = FilterAdapter<any, any, any>;
7
7
 
8
8
  /**
9
- * Base filter adapter interface that all filters must implement
10
- * This provides a consistent contract for filter behavior
11
- *
12
- * @template TData - The row data type
13
- * @template TFilterValue - The filter state value type
14
- * @template TCellValue - The actual cell value type from the table (defaults to unknown)
9
+ * Contract every filter implements. `TData` is the row type, `TFilterValue`
10
+ * the adapter's state, `TCellValue` the cell value returned by the column.
15
11
  */
16
12
  export interface FilterAdapter<TData, TFilterValue, TCellValue = unknown> {
17
- /**
18
- * Unique identifier for the filter (should match column accessorKey)
19
- */
13
+ /** Unique id, should match the column `accessorKey`. */
20
14
  id: string;
21
-
22
- /**
23
- * Display label for the filter in the navigation
24
- */
15
+ /** Display label used in the filter navigation. */
25
16
  label: string;
26
-
27
- /**
28
- * Initial/default value for the filter state
29
- */
17
+ /** Initial / reset value. */
30
18
  initialValue: TFilterValue;
31
-
32
- /**
33
- * TanStack Table filter function
34
- * Determines if a row matches the current filter value
35
- */
19
+ /** TanStack `filterFn` — returns whether a row matches the current value. */
36
20
  columnFilterFn: (row: Row<TData>, columnId: string, filterValue: TFilterValue) => boolean;
37
-
38
- /**
39
- * Optional custom cell renderer for the table column
40
- * Receives the actual cell value from the table, not the filter value
41
- * If not provided, default rendering will be used
42
- */
21
+ /** Optional cell renderer; receives the cell value, not the filter value. */
43
22
  cellRenderer?: (value: TCellValue) => ReactNode;
44
-
45
- /**
46
- * Convert filter value to filter chips for display
47
- * Returns empty array if no chips should be shown
48
- */
49
- toChips: (value: TFilterValue) => ChipItem[];
50
-
51
- /**
52
- * Remove a chip from the filter value
53
- * Returns updated filter value with the chip's effect removed
54
- */
55
- fromChip: (chip: ChipItem, currentValue: TFilterValue) => TFilterValue;
56
-
57
23
  /**
58
- * Calculate how many active filters are applied
59
- * Used for the count indicator in filter navigation
60
- * Can also be used to check if filter is active (count > 0)
24
+ * Convert the filter value into chips for `DsTagFilter`. Each chip's
25
+ * `metadata.key` / `metadata.value` must let `fromChip` undo it.
61
26
  */
27
+ toChips: (value: TFilterValue) => TagFilterItem[];
28
+ /** Remove a chip's effect from the current value. */
29
+ fromChip: (chip: TagFilterItem, currentValue: TFilterValue) => TFilterValue;
30
+ /** Number of active sub-filters; `0` means the filter is inactive. */
62
31
  getActiveFiltersCount: (value: TFilterValue) => number;
63
-
64
- /**
65
- * Reset filter to initial state
66
- */
32
+ /** Return the initial value. */
67
33
  reset: () => TFilterValue;
68
-
69
- /**
70
- * Render the filter UI component
71
- * Receives current value and onChange callback
72
- */
34
+ /** Render the filter UI for a given value + onChange. */
73
35
  renderFilter: (value: TFilterValue, onChange: (value: TFilterValue) => void) => ReactNode;
74
36
  }
75
37
 
76
- /**
77
- * Filter navigation item used to render filter tabs in the UI
78
- */
79
38
  export interface FilterNavItem {
80
- /**
81
- * Unique identifier for the filter (matches FilterAdapter.id)
82
- */
39
+ /** Matches `FilterAdapter.id`. */
83
40
  id: string;
84
- /**
85
- * Display label for the filter tab
86
- */
87
41
  label: string;
88
- /**
89
- * Optional count badge to display active filters
90
- */
42
+ /** Active filter count badge. */
91
43
  count?: number;
92
- /**
93
- * Whether this filter is disabled
94
- */
95
44
  disabled?: boolean;
96
45
  }
97
46
 
98
- /**
99
- * Filter state managed by useTableFilters hook
100
- */
47
+ /** Map of filter id → adapter value. */
101
48
  export interface FilterState<TValue> {
102
49
  [filterId: string]: TValue;
103
50
  }
104
51
 
105
- /**
106
- * Column filter for TanStack Table
107
- */
108
52
  export interface ColumnFilterState<TValue> {
109
- /**
110
- * Column identifier this filter applies to (matches the column `accessorKey`).
111
- */
53
+ /** Column id (matches the column `accessorKey`). */
112
54
  id: string;
113
- /**
114
- * Current filter value, passed to the column's filter function.
115
- */
55
+ /** Value passed to the column's filter function. */
116
56
  value: TValue;
117
57
  }
58
+
59
+ /**
60
+ * Set by the consumer on `column.meta.filter` to opt a column into the
61
+ * per-column filter popover. Provide an `adapter` inline or just `adapterId`
62
+ * to resolve against `useTableFilters({ filterAdapters })`.
63
+ */
64
+ export interface ColumnFilterMeta {
65
+ /** Mutually exclusive with `adapterId`. */
66
+ adapter?: AnyAdapter;
67
+ /** Mutually exclusive with `adapter`. */
68
+ adapterId?: string;
69
+ }
70
+
71
+ /**
72
+ * What `useTableFilters` injects onto `column.meta.filter` of each enhanced
73
+ * column. Consumers don't build this — they set `ColumnFilterMeta` and the
74
+ * hook swaps it for this resolved shape on `enhancedColumns`.
75
+ */
76
+ export interface ResolvedColumnFilter {
77
+ filterId: string;
78
+ hasActiveFilter: boolean;
79
+ /** Renders the adapter's filter UI for this column. */
80
+ render: () => ReactNode;
81
+ onApply: () => void;
82
+ onClear: () => void;
83
+ onCancel: () => void;
84
+ }
@@ -0,0 +1,81 @@
1
+ import { type KeyboardEventHandler, type MouseEvent, type MouseEventHandler } from 'react';
2
+ import type { CellContext, RowData } from '@tanstack/react-table';
3
+ import { useDsTableContext } from '../context/ds-table-context';
4
+
5
+ export interface UseCellEditorOptions<TData extends RowData, TValue> {
6
+ /**
7
+ * The TanStack Table CellContext for this cell. Pass through
8
+ * the argument received in `ColumnDef.editCell`.
9
+ */
10
+ cellContext: CellContext<TData, TValue>;
11
+ }
12
+
13
+ export interface UseCellEditorContainerProps {
14
+ onKeyDown: KeyboardEventHandler<HTMLElement>;
15
+ onClick: MouseEventHandler;
16
+ onDoubleClick: MouseEventHandler;
17
+ }
18
+
19
+ export interface UseCellEditorResult<TValue> {
20
+ /** The in-progress draft value for the active editor. */
21
+ value: TValue;
22
+ /** Validation error from the most recent commit attempt, if any. */
23
+ error: string | null;
24
+ /** Update the in-progress draft value. */
25
+ setValue: (value: TValue) => void;
26
+ /** Validate and commit the draft. */
27
+ commit: (overrideValue?: TValue) => void;
28
+ /** Discard the draft and exit edit mode. */
29
+ cancel: () => void;
30
+ /** Stop mouse events from triggering row-level handlers. */
31
+ stopPropagation: (event: MouseEvent) => void;
32
+ /** Spread on the custom `editCell` root for pointer isolation from row handlers. */
33
+ editorContainerProps: UseCellEditorContainerProps;
34
+ }
35
+
36
+ /**
37
+ * Wires a TanStack Table `editCell` renderer into DsTable's hoisted editing state.
38
+ * Only mount this hook inside `ColumnDef.editCell` — the table cell layer handles
39
+ * entering edit mode on double-click.
40
+ */
41
+ export const useCellEditor = <TData extends RowData, TValue>({
42
+ cellContext,
43
+ }: UseCellEditorOptions<TData, TValue>): UseCellEditorResult<TValue> => {
44
+ const ctx = useDsTableContext<TData, TValue>();
45
+ const { row, column, getValue } = cellContext;
46
+
47
+ const isActiveCell =
48
+ ctx.editing !== null && ctx.editing.cell.row.id === row.id && ctx.editing.cell.column.id === column.id;
49
+ const value = isActiveCell && ctx.editing ? ctx.editing.draftValue : getValue();
50
+ const error = isActiveCell && ctx.editing ? ctx.editing.error : null;
51
+
52
+ const setValue = (next: TValue) => {
53
+ ctx.setDraft(next);
54
+ };
55
+
56
+ const commit = (overrideValue?: TValue) => {
57
+ ctx.commit(overrideValue);
58
+ };
59
+
60
+ const cancel = () => {
61
+ ctx.cancel();
62
+ };
63
+
64
+ const stopPropagation = (event: MouseEvent) => {
65
+ event.stopPropagation();
66
+ };
67
+
68
+ return {
69
+ value,
70
+ error,
71
+ setValue,
72
+ commit,
73
+ cancel,
74
+ stopPropagation,
75
+ editorContainerProps: {
76
+ onKeyDown: () => {},
77
+ onClick: stopPropagation,
78
+ onDoubleClick: stopPropagation,
79
+ },
80
+ };
81
+ };
@@ -5,3 +5,32 @@ export * from './filters';
5
5
 
6
6
  export { default as DsTable } from './ds-table';
7
7
  export * from './ds-table.types';
8
+
9
+ export { useCellEditor } from './hooks/use-cell-editor';
10
+ export type {
11
+ UseCellEditorOptions,
12
+ UseCellEditorResult,
13
+ UseCellEditorContainerProps,
14
+ } from './hooks/use-cell-editor';
15
+
16
+ export { DsTableEditableCell } from './components/edit/ds-table-editable-cell';
17
+ export type { DsTableEditableCellProps } from './components/edit/ds-table-editable-cell';
18
+
19
+ export { DsTableEditingCell } from './components/edit/ds-table-editing-cell';
20
+ export type { DsTableEditingCellProps } from './components/edit/ds-table-editing-cell';
21
+
22
+ export {
23
+ DsTableEditCellText,
24
+ DsTableEditCellNumber,
25
+ DsTableEditCellSelect,
26
+ DsTableEditCellDate,
27
+ DsTableEditCellCheckbox,
28
+ } from './components/edit/cell-editors';
29
+ export type {
30
+ DsTableEditCellTextProps,
31
+ DsTableEditCellNumberProps,
32
+ DsTableEditCellSelectProps,
33
+ DsTableEditCellDateProps,
34
+ DsTableEditCellDateValue,
35
+ DsTableEditCellCheckboxProps,
36
+ } from './components/edit/cell-editors';
@@ -2,19 +2,20 @@ import type { CSSProperties } from 'react';
2
2
  import { defaultColumnSizing } from '@tanstack/react-table';
3
3
 
4
4
  /**
5
- * Generates the appropriate style object for a table column/cell based on its size and virtualization state
5
+ * Generates the appropriate style object for a table column/cell based on its size
6
+ *
7
+ * Custom-sized columns get a fixed `width` plus `minWidth` and `flexShrink: 0` so
8
+ * they enforce horizontal overflow on the table container. Default-sized columns
9
+ * grow to fill the row evenly via `flex: 1`.
6
10
  *
7
11
  * @param columnSize - The size of the column
8
- * @param virtualized - Whether the table is virtualized
9
- * @returns Style object or undefined if no custom styling is needed
12
+ * @returns Style object for the column/cell
10
13
  */
11
- export const getColumnSizeStyle = (columnSize: number, virtualized?: boolean): CSSProperties | undefined => {
14
+ export const getColumnSizeStyle = (columnSize: number): CSSProperties => {
12
15
  const hasCustomSize = columnSize !== defaultColumnSizing.size;
13
16
  if (hasCustomSize) {
14
- return { width: columnSize };
17
+ return { width: columnSize, minWidth: columnSize, flexShrink: 0 };
15
18
  }
16
19
 
17
- if (virtualized) {
18
- return { flex: 1 };
19
- }
20
+ return { flex: 1, minWidth: 0 };
20
21
  };
@@ -43,7 +43,7 @@ const DsTag = ({
43
43
  const handleKeyDownTag = (event: KeyboardEvent<HTMLElement>) => {
44
44
  if (onClick && (event.key === 'Enter' || event.key === ' ')) {
45
45
  event.preventDefault();
46
- onClick(event as KeyboardEvent<HTMLDivElement>);
46
+ onClick(event);
47
47
 
48
48
  return;
49
49
  }
@@ -66,7 +66,7 @@ const DsTag = ({
66
66
  const handleClick = (event: MouseEvent<HTMLElement>) => {
67
67
  if (onClick) {
68
68
  event.preventDefault();
69
- onClick(event as MouseEvent<HTMLDivElement>);
69
+ onClick(event);
70
70
  }
71
71
  };
72
72