@coveord/plasma-mantine 58.0.1 → 59.0.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 (67) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/.turbo/turbo-test.log +100 -103
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cjs/components/Badge/Badge.d.ts +2 -2
  5. package/dist/cjs/components/Badge/Badge.d.ts.map +1 -1
  6. package/dist/cjs/components/Badge/Badge.js.map +1 -1
  7. package/dist/cjs/components/Header/HeaderRight/HeaderRight.js +1 -1
  8. package/dist/cjs/components/Header/HeaderRight/HeaderRight.js.map +1 -1
  9. package/dist/cjs/components/LastUpdated/LastUpdated.d.ts.map +1 -1
  10. package/dist/cjs/components/LastUpdated/LastUpdated.js +0 -1
  11. package/dist/cjs/components/LastUpdated/LastUpdated.js.map +1 -1
  12. package/dist/cjs/components/Modal/ModalFooter.module.css +1 -0
  13. package/dist/cjs/components/Prompt/Prompt.d.ts +3 -3
  14. package/dist/cjs/components/Prompt/Prompt.d.ts.map +1 -1
  15. package/dist/cjs/components/Prompt/Prompt.js +6 -6
  16. package/dist/cjs/components/Prompt/Prompt.js.map +1 -1
  17. package/dist/cjs/components/Table/use-persisted-column-visibility.d.ts +16 -0
  18. package/dist/cjs/components/Table/use-persisted-column-visibility.d.ts.map +1 -0
  19. package/dist/cjs/components/Table/use-persisted-column-visibility.js +123 -0
  20. package/dist/cjs/components/Table/use-persisted-column-visibility.js.map +1 -0
  21. package/dist/cjs/components/Table/use-table.d.ts +12 -0
  22. package/dist/cjs/components/Table/use-table.d.ts.map +1 -1
  23. package/dist/cjs/components/Table/use-table.js +22 -7
  24. package/dist/cjs/components/Table/use-table.js.map +1 -1
  25. package/dist/cjs/styles/Modal.module.css +13 -0
  26. package/dist/cjs/utils/local-storage.d.ts +38 -0
  27. package/dist/cjs/utils/local-storage.d.ts.map +1 -0
  28. package/dist/cjs/utils/local-storage.js +175 -0
  29. package/dist/cjs/utils/local-storage.js.map +1 -0
  30. package/dist/esm/components/Badge/Badge.d.ts +2 -2
  31. package/dist/esm/components/Badge/Badge.d.ts.map +1 -1
  32. package/dist/esm/components/Badge/Badge.js.map +1 -1
  33. package/dist/esm/components/Header/HeaderRight/HeaderRight.js +1 -1
  34. package/dist/esm/components/Header/HeaderRight/HeaderRight.js.map +1 -1
  35. package/dist/esm/components/LastUpdated/LastUpdated.d.ts.map +1 -1
  36. package/dist/esm/components/LastUpdated/LastUpdated.js +0 -1
  37. package/dist/esm/components/LastUpdated/LastUpdated.js.map +1 -1
  38. package/dist/esm/components/Modal/ModalFooter.module.css +1 -0
  39. package/dist/esm/components/Prompt/Prompt.d.ts +3 -3
  40. package/dist/esm/components/Prompt/Prompt.d.ts.map +1 -1
  41. package/dist/esm/components/Prompt/Prompt.js +7 -8
  42. package/dist/esm/components/Prompt/Prompt.js.map +1 -1
  43. package/dist/esm/components/Table/use-persisted-column-visibility.d.ts +16 -0
  44. package/dist/esm/components/Table/use-persisted-column-visibility.d.ts.map +1 -0
  45. package/dist/esm/components/Table/use-persisted-column-visibility.js +85 -0
  46. package/dist/esm/components/Table/use-persisted-column-visibility.js.map +1 -0
  47. package/dist/esm/components/Table/use-table.d.ts +12 -0
  48. package/dist/esm/components/Table/use-table.d.ts.map +1 -1
  49. package/dist/esm/components/Table/use-table.js +15 -3
  50. package/dist/esm/components/Table/use-table.js.map +1 -1
  51. package/dist/esm/styles/Modal.module.css +13 -0
  52. package/dist/esm/utils/local-storage.d.ts +38 -0
  53. package/dist/esm/utils/local-storage.d.ts.map +1 -0
  54. package/dist/esm/utils/local-storage.js +134 -0
  55. package/dist/esm/utils/local-storage.js.map +1 -0
  56. package/package.json +5 -5
  57. package/src/components/Badge/Badge.tsx +30 -27
  58. package/src/components/Header/HeaderRight/HeaderRight.tsx +1 -1
  59. package/src/components/LastUpdated/LastUpdated.tsx +34 -36
  60. package/src/components/Modal/ModalFooter.module.css +1 -0
  61. package/src/components/Prompt/Prompt.tsx +14 -6
  62. package/src/components/Table/__tests__/use-persisted-column-visibility.spec.ts +203 -0
  63. package/src/components/Table/use-persisted-column-visibility.ts +79 -0
  64. package/src/components/Table/use-table.ts +36 -3
  65. package/src/styles/Modal.module.css +13 -0
  66. package/src/utils/__tests__/local-storage.spec.ts +176 -0
  67. package/src/utils/local-storage.ts +151 -0
@@ -4,41 +4,44 @@ import {
4
4
  BadgeProps,
5
5
  BadgeStylesNames,
6
6
  BadgeVariant,
7
+ ElementProps,
7
8
  Badge as MantineBadge,
9
+ PolymorphicComponentProps,
8
10
  polymorphicFactory,
9
11
  PolymorphicFactory,
10
- PolymorphicComponentProps,
11
12
  useComputedColorScheme,
12
13
  } from '@mantine/core';
13
14
  import {forwardRef, ForwardRefExoticComponent, ReactElement, ReactNode, RefAttributes} from 'react';
14
15
 
15
16
  export interface SemanticBadgeProps
16
- extends Pick<
17
- BadgeProps,
18
- | 'm'
19
- | 'mt'
20
- | 'mb'
21
- | 'ml'
22
- | 'mr'
23
- | 'ms'
24
- | 'me'
25
- | 'mx'
26
- | 'my'
27
- | 'miw'
28
- | 'maw'
29
- | 'pos'
30
- | 'top'
31
- | 'left'
32
- | 'right'
33
- | 'bottom'
34
- | 'inset'
35
- | 'display'
36
- | 'flex'
37
- | 'leftSection'
38
- | 'rightSection'
39
- | 'fullWidth'
40
- | 'circle'
41
- > {
17
+ extends
18
+ Pick<
19
+ BadgeProps,
20
+ | 'm'
21
+ | 'mt'
22
+ | 'mb'
23
+ | 'ml'
24
+ | 'mr'
25
+ | 'ms'
26
+ | 'me'
27
+ | 'mx'
28
+ | 'my'
29
+ | 'miw'
30
+ | 'maw'
31
+ | 'pos'
32
+ | 'top'
33
+ | 'left'
34
+ | 'right'
35
+ | 'bottom'
36
+ | 'inset'
37
+ | 'display'
38
+ | 'flex'
39
+ | 'leftSection'
40
+ | 'rightSection'
41
+ | 'fullWidth'
42
+ | 'circle'
43
+ >,
44
+ ElementProps<'div'> {
42
45
  /**
43
46
  * The size of the badge.
44
47
  * @default 'small'
@@ -19,7 +19,7 @@ export type HeaderRightFactory = Factory<{
19
19
  }>;
20
20
 
21
21
  const defaultProps: Partial<HeaderRightProps> = {
22
- gap: 'sm',
22
+ gap: 'xs',
23
23
  };
24
24
 
25
25
  export const HeaderRight = factory<HeaderRightFactory>((_props, ref) => {
@@ -1,6 +1,6 @@
1
1
  import {BoxProps, factory, Factory, Group, GroupProps, StylesApiProps, Text, useProps, useStyles} from '@mantine/core';
2
2
  import dayjs from 'dayjs';
3
- import React from 'react';
3
+ import {ForwardedRef} from 'react';
4
4
 
5
5
  export type LastUpdatedStylesNames = 'root' | 'label';
6
6
 
@@ -35,43 +35,41 @@ const defaultProps: Partial<LastUpdatedProps> = {
35
35
  formatter: (time) => dayjs(time).format('h:mm:ss A'),
36
36
  };
37
37
 
38
- export const LastUpdated = factory<LastUpdatedFactory>(
39
- (props: LastUpdatedProps, ref: React.ForwardedRef<HTMLDivElement>) => {
40
- const {formatter, label, time, classNames, className, styles, style, vars, unstyled, ...others} = useProps(
41
- 'PlasmaLastUpdated',
42
- defaultProps as Partial<LastUpdatedProps>,
43
- props,
44
- );
38
+ export const LastUpdated = factory<LastUpdatedFactory>((props: LastUpdatedProps, ref: ForwardedRef<HTMLDivElement>) => {
39
+ const {formatter, label, time, classNames, className, styles, style, vars, unstyled, ...others} = useProps(
40
+ 'PlasmaLastUpdated',
41
+ defaultProps as Partial<LastUpdatedProps>,
42
+ props,
43
+ );
45
44
 
46
- const resolvedTime = time ?? dayjs().valueOf();
45
+ const resolvedTime = time ?? dayjs().valueOf();
47
46
 
48
- const getStyles = useStyles<LastUpdatedFactory>({
49
- name: 'LastUpdated',
50
- classes: {},
51
- props,
52
- className,
53
- style,
54
- classNames,
55
- styles,
56
- unstyled,
57
- vars,
58
- });
59
- const stylesApiProps = {classNames, styles};
47
+ const getStyles = useStyles<LastUpdatedFactory>({
48
+ name: 'LastUpdated',
49
+ classes: {},
50
+ props,
51
+ className,
52
+ style,
53
+ classNames,
54
+ styles,
55
+ unstyled,
56
+ vars,
57
+ });
58
+ const stylesApiProps = {classNames, styles};
60
59
 
61
- return (
62
- <Group
63
- justify={props.justify}
64
- ref={ref}
65
- {...getStyles('root', {className, style, ...stylesApiProps})}
66
- {...others}
67
- >
68
- <Text size="xs" {...getStyles('label', {className, style, ...stylesApiProps})}>
69
- {label}
70
- <span role="timer">{formatter(resolvedTime)}</span>
71
- </Text>
72
- </Group>
73
- );
74
- },
75
- );
60
+ return (
61
+ <Group
62
+ justify={props.justify}
63
+ ref={ref}
64
+ {...getStyles('root', {className, style, ...stylesApiProps})}
65
+ {...others}
66
+ >
67
+ <Text size="xs" {...getStyles('label', {className, style, ...stylesApiProps})}>
68
+ {label}
69
+ <span role="timer">{formatter(resolvedTime)}</span>
70
+ </Text>
71
+ </Group>
72
+ );
73
+ });
76
74
 
77
75
  LastUpdated.displayName = 'LastUpdated';
@@ -2,6 +2,7 @@
2
2
  .root {
3
3
  margin: calc(-1 * var(--mb-padding));
4
4
  margin-top: var(--mb-padding);
5
+ bottom: calc(-1 * var(--mb-padding));
5
6
  }
6
7
  }
7
8
 
@@ -1,5 +1,4 @@
1
1
  import {
2
- Box,
3
2
  factory,
4
3
  Factory,
5
4
  ModalRootProps,
@@ -9,7 +8,16 @@ import {
9
8
  useProps,
10
9
  useStyles,
11
10
  } from '@mantine/core';
12
- import {Children, ComponentProps, ComponentType, forwardRef, FunctionComponent, ReactElement, ReactNode} from 'react';
11
+ import {
12
+ Children,
13
+ ComponentProps,
14
+ ComponentType,
15
+ forwardRef,
16
+ ForwardRefExoticComponent,
17
+ ReactElement,
18
+ ReactNode,
19
+ RefAttributes,
20
+ } from 'react';
13
21
  import {InfoToken} from '../InfoToken/InfoToken.js';
14
22
  import {Modal} from '../Modal/Modal.js';
15
23
  import {PromptContextProvider} from './Prompt.context.js';
@@ -114,9 +122,9 @@ const _Prompt = factory<PromptFactory>((_props, ref) => {
114
122
  <Modal.CloseButton {...getStyles('close', stylesApiProps)} />
115
123
  </Modal.Header>
116
124
  <Modal.Body {...getStyles('body', stylesApiProps)}>
117
- <Box {...getStyles('inner', stylesApiProps)}>{otherChildren}</Box>
125
+ {otherChildren}
126
+ {footers}
118
127
  </Modal.Body>
119
- {footers}
120
128
  </Modal.Content>
121
129
  </Modal.Root>
122
130
  </PromptContextProvider>
@@ -124,12 +132,12 @@ const _Prompt = factory<PromptFactory>((_props, ref) => {
124
132
  });
125
133
  _Prompt.displayName = 'Prompt';
126
134
 
127
- type PromptCompoundComponent = ((props: PromptProps) => ReactElement) & Omit<FunctionComponent<PromptProps>, never>;
135
+ type PromptCompoundComponent = ForwardRefExoticComponent<PromptProps & RefAttributes<HTMLDivElement>>;
128
136
 
129
137
  const PromptFooter: ComponentType<ComponentProps<typeof Modal.Footer>> = (props) => <Modal.Footer {...props} />;
130
138
  PromptFooter.displayName = 'Prompt.Footer';
131
139
 
132
- const createPromptCompound = (variant: PromptVariant, displayName: string): PromptCompoundComponent => {
140
+ const createPromptCompound = (variant: PromptVariant, displayName: string) => {
133
141
  const Component = forwardRef<HTMLDivElement, PromptProps>((props, ref) => (
134
142
  <_Prompt ref={ref} {...props} variant={variant} />
135
143
  ));
@@ -0,0 +1,203 @@
1
+ import {act, renderHook} from '@test-utils';
2
+ import {CURRENT_STORAGE_VERSION, STORAGE_KEY} from '../../../utils/local-storage.js';
3
+ import {usePersistedColumnVisibility} from '../use-persisted-column-visibility.js';
4
+ import {useTable} from '../use-table.js';
5
+
6
+ const setStoredVisibility = (tableId: string, value: unknown) => {
7
+ const existing = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null') ?? {
8
+ 'storage-version': CURRENT_STORAGE_VERSION,
9
+ storage: {},
10
+ };
11
+ if (!existing.storage.table) {
12
+ existing.storage.table = {};
13
+ }
14
+ if (!existing.storage.table[tableId]) {
15
+ existing.storage.table[tableId] = {};
16
+ }
17
+ existing.storage.table[tableId].columnVisibility = value;
18
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(existing));
19
+ };
20
+
21
+ const getStoredVisibility = (tableId: string): unknown => {
22
+ const data = JSON.parse(localStorage.getItem(STORAGE_KEY)!);
23
+ return data?.storage?.table?.[tableId]?.columnVisibility ?? null;
24
+ };
25
+
26
+ describe('usePersistedColumnVisibility', () => {
27
+ afterEach(() => {
28
+ localStorage.clear();
29
+ });
30
+
31
+ describe('without tableId', () => {
32
+ it('returns the default visibility and does not persist', () => {
33
+ const defaults = {col1: true, col2: false};
34
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity));
35
+
36
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
37
+
38
+ act(() => {
39
+ result.current.persistColumnVisibility({col1: false});
40
+ });
41
+
42
+ expect(localStorage.length).toBe(0);
43
+ });
44
+ });
45
+
46
+ describe('with tableId', () => {
47
+ const tableId = 'test-table';
48
+
49
+ it('returns the default visibility when nothing is stored', () => {
50
+ const defaults = {col1: true, col2: true, col3: false};
51
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
52
+
53
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: true, col3: false});
54
+ });
55
+
56
+ it('merges stored visibility with defaults', () => {
57
+ setStoredVisibility(tableId, {col2: false});
58
+ const defaults = {col1: true, col2: true, col3: false};
59
+
60
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
61
+
62
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false, col3: false});
63
+ });
64
+
65
+ it('ignores stored keys not present in defaults and non-boolean values', () => {
66
+ setStoredVisibility(tableId, {unknown: true, col1: 'yes', col2: false});
67
+ const defaults = {col1: true, col2: true};
68
+
69
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
70
+
71
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
72
+ });
73
+
74
+ it('caps visible columns to maxSelectableColumns from both defaults and storage', () => {
75
+ setStoredVisibility(tableId, {col1: true, col2: true, col3: true});
76
+ const defaults = {col1: false, col2: false, col3: false};
77
+
78
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, 2, tableId));
79
+
80
+ const visibleCount = Object.values(result.current.initialColumnVisibility).filter(Boolean).length;
81
+ expect(visibleCount).toBe(2);
82
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: true, col3: false});
83
+ });
84
+
85
+ it('persists visibility to localStorage', () => {
86
+ const defaults = {col1: true, col2: true};
87
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
88
+
89
+ act(() => {
90
+ result.current.persistColumnVisibility({col1: false, col2: true});
91
+ });
92
+
93
+ expect(getStoredVisibility(tableId)).toEqual({col1: false, col2: true});
94
+ });
95
+
96
+ it('clears invalid stored data and falls back to defaults', () => {
97
+ localStorage.setItem(STORAGE_KEY, 'not-valid-json{{{');
98
+ const defaults = {col1: true, col2: false};
99
+
100
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
101
+
102
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
103
+ expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
104
+ });
105
+
106
+ it.each([
107
+ ['an array', [true, false]],
108
+ ['null', null],
109
+ ['an empty object', {}],
110
+ ])('falls back to defaults when stored value is %s', (_, storedValue) => {
111
+ setStoredVisibility(tableId, storedValue);
112
+ const defaults = {col1: true, col2: false};
113
+
114
+ const {result} = renderHook(() => usePersistedColumnVisibility(defaults, Infinity, tableId));
115
+
116
+ expect(result.current.initialColumnVisibility).toEqual({col1: true, col2: false});
117
+ });
118
+
119
+ it('skips localStorage when defaultVisibleColumns is empty', () => {
120
+ setStoredVisibility(tableId, {col1: true});
121
+
122
+ const {result} = renderHook(() => usePersistedColumnVisibility({}, Infinity, tableId));
123
+
124
+ expect(result.current.initialColumnVisibility).toEqual({});
125
+
126
+ act(() => {
127
+ result.current.persistColumnVisibility({col1: false});
128
+ });
129
+
130
+ expect(getStoredVisibility(tableId)).toEqual({col1: true});
131
+ });
132
+ });
133
+ });
134
+
135
+ describe('useTable column visibility persistence', () => {
136
+ const tableId = 'integration-table';
137
+
138
+ afterEach(() => {
139
+ localStorage.clear();
140
+ });
141
+
142
+ it('initializes column visibility from localStorage when tableId is provided', () => {
143
+ setStoredVisibility(tableId, {col1: false, col2: true});
144
+
145
+ const {result} = renderHook(() =>
146
+ useTable({
147
+ tableId,
148
+ initialState: {columnVisibility: {col1: true, col2: false}},
149
+ }),
150
+ );
151
+
152
+ expect(result.current.state.columnVisibility).toEqual({col1: false, col2: true});
153
+ });
154
+
155
+ it('does not read from or write to localStorage when no tableId is provided', () => {
156
+ const {result} = renderHook(() =>
157
+ useTable({
158
+ initialState: {columnVisibility: {col1: true, col2: true}},
159
+ }),
160
+ );
161
+
162
+ expect(result.current.state.columnVisibility).toEqual({col1: true, col2: true});
163
+
164
+ act(() => {
165
+ result.current.setColumnVisibility({col1: false});
166
+ });
167
+
168
+ expect(result.current.state.columnVisibility).toEqual({col1: false});
169
+ expect(localStorage.length).toBe(0);
170
+ });
171
+
172
+ it('persists to localStorage when setColumnVisibility is called with a value', () => {
173
+ const {result} = renderHook(() =>
174
+ useTable({
175
+ tableId,
176
+ initialState: {columnVisibility: {col1: true, col2: true}},
177
+ }),
178
+ );
179
+
180
+ act(() => {
181
+ result.current.setColumnVisibility({col1: false, col2: true});
182
+ });
183
+
184
+ expect(result.current.state.columnVisibility).toEqual({col1: false, col2: true});
185
+ expect(getStoredVisibility(tableId)).toEqual({col1: false, col2: true});
186
+ });
187
+
188
+ it('persists to localStorage when setColumnVisibility is called with an updater function', () => {
189
+ const {result} = renderHook(() =>
190
+ useTable({
191
+ tableId,
192
+ initialState: {columnVisibility: {col1: true, col2: false}},
193
+ }),
194
+ );
195
+
196
+ act(() => {
197
+ result.current.setColumnVisibility((prev) => ({...prev, col2: true}));
198
+ });
199
+
200
+ expect(result.current.state.columnVisibility).toEqual({col1: true, col2: true});
201
+ expect(getStoredVisibility(tableId)).toEqual({col1: true, col2: true});
202
+ });
203
+ });
@@ -0,0 +1,79 @@
1
+ import {useCallback, useMemo} from 'react';
2
+ import {getStorageItem, setStorageItem} from '../../utils/local-storage.js';
3
+ import type {TableState} from './use-table.js';
4
+
5
+ type ColumnVisibility = TableState['columnVisibility'];
6
+
7
+ const storagePath = (tableId: string) => ['table', tableId, 'columnVisibility'] as const;
8
+
9
+ const capVisibleColumns = (visibility: ColumnVisibility, max: number): ColumnVisibility => {
10
+ let visibleCount = 0;
11
+ const result: ColumnVisibility = {};
12
+ for (const [key, isVisible] of Object.entries(visibility)) {
13
+ const shouldShow = isVisible && visibleCount < max;
14
+ result[key] = shouldShow;
15
+ if (shouldShow) {
16
+ visibleCount++;
17
+ }
18
+ }
19
+ return result;
20
+ };
21
+
22
+ const sanitizeFromStorage = (raw: unknown, validColumnIds: Set<string>): ColumnVisibility => {
23
+ const result: ColumnVisibility = {};
24
+
25
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
26
+ return result;
27
+ }
28
+
29
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
30
+ if (validColumnIds.has(key) && typeof value === 'boolean') {
31
+ result[key] = value;
32
+ }
33
+ }
34
+ return result;
35
+ };
36
+
37
+ /**
38
+ * Hook that persists column visibility preferences to localStorage.
39
+ *
40
+ * @param defaultVisibleColumns - The default visibility map. Its keys define the set of valid column IDs.
41
+ * Must be a stable reference (e.g. via `useRef`) to avoid re-reading localStorage on every render.
42
+ * @param maxSelectableColumns - Maximum number of columns that can be visible at the same time.
43
+ * @param tableId - Unique identifier for the table. When omitted, no persistence occurs.
44
+ */
45
+ export const usePersistedColumnVisibility = (
46
+ defaultVisibleColumns: ColumnVisibility,
47
+ maxSelectableColumns: number,
48
+ tableId?: string,
49
+ ) => {
50
+ const path = tableId ? storagePath(tableId) : null;
51
+ const validIds = useMemo(() => new Set(Object.keys(defaultVisibleColumns)), [defaultVisibleColumns]);
52
+ const hasValidIds = validIds.size > 0;
53
+
54
+ const initialColumnVisibility = useMemo((): ColumnVisibility => {
55
+ if (!path || !hasValidIds) {
56
+ return defaultVisibleColumns;
57
+ }
58
+ const stored = getStorageItem<unknown>([...path]);
59
+ if (stored !== null) {
60
+ const sanitized = sanitizeFromStorage(stored, validIds);
61
+ if (Object.keys(sanitized).length > 0) {
62
+ return capVisibleColumns({...defaultVisibleColumns, ...sanitized}, maxSelectableColumns);
63
+ }
64
+ }
65
+ return capVisibleColumns(defaultVisibleColumns, maxSelectableColumns);
66
+ }, [path, validIds, defaultVisibleColumns, maxSelectableColumns]);
67
+
68
+ const persistColumnVisibility = useCallback(
69
+ (visibility: ColumnVisibility) => {
70
+ if (!path || !hasValidIds) {
71
+ return;
72
+ }
73
+ setStorageItem([...path], visibility);
74
+ },
75
+ [path, hasValidIds],
76
+ );
77
+
78
+ return {initialColumnVisibility, persistColumnVisibility};
79
+ };
@@ -4,6 +4,7 @@ import {type ExpandedState, type PaginationState, type SortingState} from '@tans
4
4
  import defaultsDeep from 'lodash.defaultsdeep';
5
5
  import {Dispatch, SetStateAction, useCallback, useMemo, useState} from 'react';
6
6
  import {useUrlSyncedState, UseUrlSyncedStateOptions} from '../../hooks/use-url-synced-state.js';
7
+ import {usePersistedColumnVisibility} from './use-persisted-column-visibility.js';
7
8
 
8
9
  // Create a deeply optional version of another type
9
10
  type DeepPartial<T> = {
@@ -191,6 +192,18 @@ export interface UseTableOptions<TData = unknown> {
191
192
  * @default false
192
193
  */
193
194
  syncWithUrl?: boolean;
195
+ /**
196
+ * Unique identifier for the table. When provided, column visibility preferences are persisted to localStorage.
197
+ */
198
+ tableId?: string;
199
+ /**
200
+ * Maximum number of columns that can be visible when restoring persisted visibility from localStorage.
201
+ * This only affects the initial column visibility resolved on mount when `tableId` is set.
202
+ * It does not enforce a runtime limit on `setColumnVisibility` — use `TableColumnsSelector` for UI enforcement.
203
+ *
204
+ * @default Infinity
205
+ */
206
+ maxSelectableColumns?: number;
194
207
  }
195
208
 
196
209
  const defaultOptions: UseTableOptions = {
@@ -317,7 +330,9 @@ const COLUMN_VISIBILITY_SERIALIZATION = serialization<'columnVisibility'>({
317
330
 
318
331
  export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): TableStore<TData> => {
319
332
  const options = defaultsDeep({}, userOptions, defaultOptions) as UseTableOptions<TData>;
320
- const initialState = defaultsDeep({}, options.initialState, defaultState) as TableState<TData>;
333
+ const [initialState] = useState(
334
+ () => defaultsDeep({}, userOptions.initialState, defaultState) as TableState<TData>,
335
+ );
321
336
  /**
322
337
  * The `useUrlSyncedState` hook defaults to synchronize, but the table wants to default to not synchronize,
323
338
  * so always pass the sync option as a resolved boolean value.
@@ -355,12 +370,30 @@ export const useTable = <TData>(userOptions: UseTableOptions<TData> = {}): Table
355
370
  initialState: initialState.dateRange,
356
371
  sync,
357
372
  });
358
- const [columnVisibility, setColumnVisibility] = useUrlSyncedState<TableState<TData>['columnVisibility']>({
373
+
374
+ const {initialColumnVisibility, persistColumnVisibility} = usePersistedColumnVisibility(
375
+ initialState.columnVisibility,
376
+ options.maxSelectableColumns ?? Infinity,
377
+ options.tableId,
378
+ );
379
+
380
+ const [columnVisibility, _setColumnVisibility] = useUrlSyncedState<TableState<TData>['columnVisibility']>({
359
381
  ...COLUMN_VISIBILITY_SERIALIZATION,
360
- initialState: initialState.columnVisibility,
382
+ initialState: initialColumnVisibility,
361
383
  sync,
362
384
  });
363
385
 
386
+ const setColumnVisibility: typeof _setColumnVisibility = useCallback(
387
+ (updater) => {
388
+ _setColumnVisibility((old) => {
389
+ const newVis = updater instanceof Function ? updater(old) : updater;
390
+ persistColumnVisibility(newVis);
391
+ return newVis;
392
+ });
393
+ },
394
+ [_setColumnVisibility, persistColumnVisibility],
395
+ );
396
+
364
397
  // unsynced
365
398
  const [totalEntries, _setTotalEntries] = useState<TableState<TData>['totalEntries']>(initialState.totalEntries);
366
399
  const [unfilteredTotalEntries, setUnfilteredTotalEntries] = useState<TableState<TData>['totalEntries']>(
@@ -4,8 +4,21 @@
4
4
 
5
5
  .header {
6
6
  gap: var(--mantine-spacing-sm);
7
+ min-height: unset;
7
8
  }
8
9
 
9
10
  .close {
10
11
  align-self: flex-start;
11
12
  }
13
+
14
+ .content {
15
+ overflow-y: initial;
16
+ display: flex;
17
+ flex-direction: column;
18
+ }
19
+
20
+ .body {
21
+ overflow-y: scroll;
22
+ border-end-start-radius: var(--modal-radius, var(--mantine-radius-default));
23
+ border-end-end-radius: var(--modal-radius, var(--mantine-radius-default));
24
+ }