@drivenets/design-system 0.8.0 → 0.9.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 (59) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.cjs +1559 -1074
  3. package/dist/index.d.cts +559 -135
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +559 -135
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1547 -1076
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +12 -11
  11. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +0 -1
  12. package/src/components/ds-button/versions/ds-button-new/ds-button-new.module.scss +25 -9
  13. package/src/components/ds-button/versions/ds-button-new/ds-button-new.tsx +5 -5
  14. package/src/components/ds-button/versions/ds-button-new/ds-button-new.types.ts +11 -1
  15. package/src/components/ds-button/versions/ds-button-new/index.ts +4 -1
  16. package/src/components/ds-button-v3/ds-button-v3.module.scss +391 -0
  17. package/src/components/ds-button-v3/ds-button-v3.tsx +55 -0
  18. package/src/components/ds-button-v3/ds-button-v3.types.ts +49 -0
  19. package/src/components/ds-button-v3/index.ts +2 -0
  20. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.tsx +1 -1
  21. package/src/components/ds-comments-drawer/ds-comments-drawer.tsx +3 -9
  22. package/src/components/ds-date-picker/ds-date-picker.module.scss +10 -0
  23. package/src/components/ds-date-picker/ds-date-picker.tsx +4 -2
  24. package/src/components/ds-date-range-picker/ds-date-range-picker.tsx +2 -2
  25. package/src/components/ds-drawer/ds-drawer.tsx +6 -9
  26. package/src/components/ds-drawer/ds-drawer.types.ts +4 -2
  27. package/src/components/ds-form-control/ds-form-control.tsx +2 -3
  28. package/src/components/ds-panel/ds-panel.module.scss +4 -1
  29. package/src/components/ds-panel/ds-panel.tsx +8 -3
  30. package/src/components/ds-panel/ds-panel.types.ts +8 -1
  31. package/src/components/ds-panel/index.ts +4 -1
  32. package/src/components/ds-select/ds-select.module.scss +4 -1
  33. package/src/components/ds-split-button/ds-split-button.module.scss +99 -0
  34. package/src/components/ds-split-button/ds-split-button.tsx +49 -0
  35. package/src/components/ds-split-button/ds-split-button.types.ts +25 -0
  36. package/src/components/ds-split-button/index.ts +10 -0
  37. package/src/components/ds-stack/ds-stack.module.scss +4 -0
  38. package/src/components/ds-stack/ds-stack.tsx +35 -0
  39. package/src/components/ds-stack/ds-stack.types.ts +19 -0
  40. package/src/components/ds-stack/index.ts +18 -0
  41. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -2
  42. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +5 -8
  43. package/src/components/ds-table/ds-table.module.scss +6 -0
  44. package/src/components/ds-table/ds-table.tsx +3 -0
  45. package/src/components/ds-time-picker/ds-time-picker.module.scss +11 -0
  46. package/src/components/ds-time-picker/ds-time-picker.tsx +32 -12
  47. package/src/components/ds-time-picker/ds-time-picker.types.ts +6 -0
  48. package/src/index.ts +4 -0
  49. package/src/styles/_breakpoints.module.scss +5 -0
  50. package/src/styles/_responsive.scss +13 -0
  51. package/src/styles/_variables.scss +2 -0
  52. package/src/styles/shared/_button.scss +2 -0
  53. package/src/styles/shared/_input.scss +3 -40
  54. package/src/utils/responsive.browser.test.ts +94 -0
  55. package/src/utils/responsive.ts +79 -0
  56. package/src/utils/responsive.unit.test.ts +54 -0
  57. package/src/utils/type-utils.ts +1 -0
  58. package/src/utils/use-controlled.browser.test.ts +6 -6
  59. package/src/utils/use-controlled.ts +1 -1
@@ -1,6 +1,7 @@
1
1
  import { type ChangeEvent, Fragment, useEffect, useRef, useState } from 'react';
2
2
  import { Popover } from '@ark-ui/react/popover';
3
3
  import { Portal } from '@ark-ui/react/portal';
4
+ import classNames from 'classnames';
4
5
  import type { DsTimePickerProps } from './ds-time-picker.types';
5
6
  import { clampTime, formatTime, parseTime, timeScrollerAdapter } from './ds-time-picker.utils';
6
7
  import { TimeScroller } from './components/time-scroller/time-scroller';
@@ -8,6 +9,7 @@ import { DsIcon } from '../ds-icon';
8
9
  import { DsButton } from '../ds-button';
9
10
  import { DsTextInput } from '../ds-text-input';
10
11
  import { useControlled } from '../../utils/use-controlled';
12
+ import styles from './ds-time-picker.module.scss';
11
13
 
12
14
  const DsTimePicker = (props: DsTimePickerProps) => {
13
15
  const {
@@ -19,6 +21,7 @@ const DsTimePicker = (props: DsTimePickerProps) => {
19
21
  max,
20
22
  disabled,
21
23
  readOnly,
24
+ hideClearButton = false,
22
25
  disablePortal = false,
23
26
  locale,
24
27
  slotProps,
@@ -103,6 +106,8 @@ const DsTimePicker = (props: DsTimePickerProps) => {
103
106
  setIsOpen(details.open);
104
107
  };
105
108
 
109
+ const showClearButton = !hideClearButton && !disabled && !readOnly && !!value;
110
+
106
111
  const Wrapper = disablePortal ? Fragment : Portal;
107
112
 
108
113
  return (
@@ -112,7 +117,7 @@ const DsTimePicker = (props: DsTimePickerProps) => {
112
117
  positioning={{ placement: 'bottom-start', gutter: 4 }}
113
118
  >
114
119
  <Popover.Anchor asChild>
115
- <div ref={ref} className={className}>
120
+ <div ref={ref} className={classNames(styles.root, className)}>
116
121
  <DsTextInput
117
122
  ref={inputRef}
118
123
  id={id}
@@ -126,17 +131,32 @@ const DsTimePicker = (props: DsTimePickerProps) => {
126
131
  {...slotProps?.input}
127
132
  slots={{
128
133
  endAdornment: slotProps?.input?.slots?.endAdornment ?? (
129
- <Popover.Trigger asChild>
130
- <DsButton
131
- design="v1.2"
132
- size="tiny"
133
- buttonType="tertiary"
134
- disabled={disabled || readOnly}
135
- aria-label={locale?.openLabel ?? 'Open time picker'}
136
- >
137
- <DsIcon icon="schedule" variant="outlined" size="tiny" />
138
- </DsButton>
139
- </Popover.Trigger>
134
+ <>
135
+ {showClearButton && (
136
+ <DsButton
137
+ className={styles.clearTrigger}
138
+ design="v1.2"
139
+ size="tiny"
140
+ buttonType="tertiary"
141
+ disabled={disabled}
142
+ aria-label={locale?.clearLabel ?? 'Clear time'}
143
+ onClick={() => setValue(null)}
144
+ >
145
+ <DsIcon icon="close" size="tiny" />
146
+ </DsButton>
147
+ )}
148
+ <Popover.Trigger asChild>
149
+ <DsButton
150
+ design="v1.2"
151
+ size="tiny"
152
+ buttonType="tertiary"
153
+ disabled={disabled || readOnly}
154
+ aria-label={locale?.openLabel ?? 'Open time picker'}
155
+ >
156
+ <DsIcon icon="schedule" variant="outlined" size="tiny" />
157
+ </DsButton>
158
+ </Popover.Trigger>
159
+ </>
140
160
  ),
141
161
  ...slotProps?.input?.slots,
142
162
  }}
@@ -21,6 +21,12 @@ export interface DsTimePickerProps {
21
21
  disabled?: boolean;
22
22
  readOnly?: boolean;
23
23
 
24
+ /**
25
+ * Whether to hide the clear button
26
+ * @default false
27
+ */
28
+ hideClearButton?: boolean;
29
+
24
30
  /**
25
31
  * Whether to disable the portal for the popover content
26
32
  * @default false
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './components/ds-avatar';
4
4
  export * from './components/ds-avatar-group';
5
5
  export * from './components/ds-breadcrumb';
6
6
  export * from './components/ds-button';
7
+ export * from './components/ds-button-v3';
7
8
  export * from './components/ds-card';
8
9
  export * from './components/ds-checkbox';
9
10
  export * from './components/ds-chip';
@@ -42,6 +43,8 @@ export * from './components/ds-select';
42
43
  export * from './components/ds-skeleton';
43
44
  export * from './components/ds-smart-tabs';
44
45
  export * from './components/ds-spinner';
46
+ export * from './components/ds-split-button';
47
+ export * from './components/ds-stack';
45
48
  export * from './components/ds-status-badge';
46
49
  export * from './components/ds-stepper';
47
50
  export * from './components/ds-system-status';
@@ -58,3 +61,4 @@ export * from './components/ds-tooltip';
58
61
  export * from './components/ds-tree';
59
62
  export * from './components/ds-typography';
60
63
  export * from './components/ds-vertical-tabs';
64
+ export * from './utils/responsive';
@@ -0,0 +1,5 @@
1
+ @use 'variables' as var;
2
+
3
+ :export {
4
+ breakpointLg: #{var.$breakpoint-lg};
5
+ }
@@ -0,0 +1,13 @@
1
+ @use 'variables' as var;
2
+
3
+ @mixin lg {
4
+ @media (min-width: #{var.$breakpoint-lg}) {
5
+ @content;
6
+ }
7
+ }
8
+
9
+ @mixin md {
10
+ @media (max-width: #{var.$breakpoint-lg - 1}) {
11
+ @content;
12
+ }
13
+ }
@@ -42,3 +42,5 @@ $line-height-action-small: 20px;
42
42
  $sidebar-spacing: 10px;
43
43
  $sidebar-width: 64px;
44
44
  $sidebar-width-expanded: 256px;
45
+
46
+ $breakpoint-lg: 1440px;
@@ -0,0 +1,2 @@
1
+ $transition-duration-default: 0.3s; // it looks a bit better with 0.3 than 0.2
2
+ $transition-duration-quick: 0.15s;
@@ -57,8 +57,7 @@ $textarea-height-default: 120px;
57
57
 
58
58
  &:has(.input:disabled) {
59
59
  color: var(--color-background-deselected);
60
- background: var(--color-background-secondary);
61
- border-color: var(--color-border-disabled);
60
+ border-color: var(--border-border-disabled);
62
61
  cursor: not-allowed;
63
62
 
64
63
  .input {
@@ -82,26 +81,7 @@ $textarea-height-default: 120px;
82
81
 
83
82
  &:disabled {
84
83
  color: var(--color-background-deselected);
85
- background: var(--color-background-secondary);
86
- border-color: var(--color-border-disabled);
87
- cursor: not-allowed;
88
- }
89
- }
90
-
91
- @mixin form-control-states {
92
- &:hover:not(:disabled) {
93
- border-color: var(--form-control-border-color, var(--color-background-action-hover));
94
- }
95
-
96
- &:focus {
97
- border-color: var(--form-control-border-color, var(--color-border-action-primary));
98
- outline: none;
99
- }
100
-
101
- &:disabled {
102
- color: var(--color-background-deselected);
103
- background: var(--color-background-secondary);
104
- border-color: var(--color-border-disabled);
84
+ border-color: var(--border-border-disabled);
105
85
  cursor: not-allowed;
106
86
  }
107
87
  }
@@ -129,8 +109,7 @@ $textarea-height-default: 120px;
129
109
 
130
110
  &[data-disabled] {
131
111
  color: var(--color-background-deselected);
132
- background: var(--color-background-secondary);
133
- border-color: var(--color-border-disabled);
112
+ border-color: var(--border-border-disabled);
134
113
 
135
114
  &,
136
115
  & * {
@@ -245,22 +224,6 @@ $textarea-height-default: 120px;
245
224
  }
246
225
  }
247
226
 
248
- // Form control base styles
249
- @mixin formControl {
250
- @include base-input-container;
251
- @include form-control-states;
252
- cursor: pointer;
253
- justify-content: space-between;
254
- }
255
-
256
- @mixin formControlSmall {
257
- max-height: $input-text-height-small;
258
- }
259
-
260
- @mixin formControlDefault {
261
- max-height: $input-text-height-default;
262
- }
263
-
264
227
  // Select trigger styles
265
228
  @mixin selectControl {
266
229
  display: grid;
@@ -0,0 +1,94 @@
1
+ import { createElement } from 'react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { page } from 'vitest/browser';
4
+ import { renderHook } from 'vitest-browser-react';
5
+
6
+ import { useBreakpoint, useResponsiveValue, withResponsiveProps } from './responsive';
7
+
8
+ const mockMatchMedia = (matches: boolean) => {
9
+ const listeners: Array<() => void> = [];
10
+
11
+ Object.defineProperty(window, 'matchMedia', {
12
+ writable: true,
13
+ value: vi.fn().mockReturnValue({
14
+ matches,
15
+ addEventListener: (_: string, cb: () => void) => listeners.push(cb),
16
+ removeEventListener: (_: string, cb: () => void) => {
17
+ const idx = listeners.indexOf(cb);
18
+
19
+ if (idx >= 0) {
20
+ listeners.splice(idx, 1);
21
+ }
22
+ },
23
+ }),
24
+ });
25
+
26
+ return {
27
+ triggerChange: () => listeners.forEach((cb) => cb()),
28
+ };
29
+ };
30
+
31
+ describe('useBreakpoint', () => {
32
+ it('should return lg when viewport >= 1440px', async () => {
33
+ mockMatchMedia(true);
34
+ const { result } = await renderHook(() => useBreakpoint());
35
+
36
+ expect(result.current).toBe('lg');
37
+ });
38
+
39
+ it('should return md when viewport < 1440px', async () => {
40
+ mockMatchMedia(false);
41
+ const { result } = await renderHook(() => useBreakpoint());
42
+
43
+ expect(result.current).toBe('md');
44
+ });
45
+ });
46
+
47
+ describe('useResponsiveValue', () => {
48
+ it('should return a static value unchanged', async () => {
49
+ mockMatchMedia(true);
50
+ const { result } = await renderHook(() => useResponsiveValue('large'));
51
+
52
+ expect(result.current).toBe('large');
53
+ });
54
+
55
+ it('should resolve lg value on large screens', async () => {
56
+ mockMatchMedia(true);
57
+ const { result } = await renderHook(() => useResponsiveValue({ lg: 'large', md: 'small' }));
58
+
59
+ expect(result.current).toBe('large');
60
+ });
61
+
62
+ it('should resolve md value on small screens', async () => {
63
+ mockMatchMedia(false);
64
+ const { result } = await renderHook(() => useResponsiveValue({ lg: 'large', md: 'small' }));
65
+
66
+ expect(result.current).toBe('small');
67
+ });
68
+ });
69
+
70
+ describe('withResponsiveProps', () => {
71
+ const Base = ({ value }: { value?: string }) => createElement('span', null, value);
72
+ const Enhanced = withResponsiveProps(Base, ['value']);
73
+
74
+ it('should resolve responsive prop to lg value on large screens', async () => {
75
+ mockMatchMedia(true);
76
+ await page.render(createElement(Enhanced, { value: { lg: 'desktop', md: 'mobile' } }));
77
+
78
+ await expect.element(page.getByText('desktop')).toBeInTheDocument();
79
+ });
80
+
81
+ it('should resolve responsive prop to md value on small screens', async () => {
82
+ mockMatchMedia(false);
83
+ await page.render(createElement(Enhanced, { value: { lg: 'desktop', md: 'mobile' } }));
84
+
85
+ await expect.element(page.getByText('mobile')).toBeInTheDocument();
86
+ });
87
+
88
+ it('should pass through static values unchanged', async () => {
89
+ mockMatchMedia(true);
90
+ await page.render(createElement(Enhanced, { value: 'static' }));
91
+
92
+ await expect.element(page.getByText('static')).toBeInTheDocument();
93
+ });
94
+ });
@@ -0,0 +1,79 @@
1
+ import { createElement, useSyncExternalStore, type FunctionComponent } from 'react';
2
+
3
+ import breakpointTokens from '../styles/_breakpoints.module.scss';
4
+
5
+ export const BREAKPOINT_LG = parseInt(breakpointTokens.breakpointLg, 10);
6
+
7
+ export const breakpoints = ['lg', 'md'] as const;
8
+ export type Breakpoint = (typeof breakpoints)[number];
9
+
10
+ export type Responsive<T> = Partial<Record<Breakpoint, T>>;
11
+
12
+ export type ResponsiveValue<T> = T | Responsive<T>;
13
+
14
+ export const isResponsiveValue = <T>(value: ResponsiveValue<T>): value is Responsive<T> =>
15
+ value !== null && typeof value === 'object' && breakpoints.some((bp) => bp in value);
16
+
17
+ export const resolveResponsiveValue = <T>(value: ResponsiveValue<T>, breakpoint: Breakpoint): T => {
18
+ if (!isResponsiveValue(value)) {
19
+ return value;
20
+ }
21
+
22
+ return (value[breakpoint] ?? value.lg ?? value.md) as T;
23
+ };
24
+
25
+ const MEDIA_QUERY = `(min-width: ${String(BREAKPOINT_LG)}px)`;
26
+
27
+ const subscribe = (callback: () => void) => {
28
+ const mql = window.matchMedia(MEDIA_QUERY);
29
+ mql.addEventListener('change', callback);
30
+ return () => mql.removeEventListener('change', callback);
31
+ };
32
+
33
+ const getSnapshot = (): Breakpoint => (window.matchMedia(MEDIA_QUERY).matches ? 'lg' : 'md');
34
+
35
+ const getServerSnapshot = (): Breakpoint => 'lg';
36
+
37
+ export const useBreakpoint = (): Breakpoint =>
38
+ useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
39
+
40
+ /**
41
+ * Resolves a `ResponsiveValue<T>` to `T` using the current breakpoint.
42
+ * Use for conditional rendering or JS logic that depends on the breakpoint.
43
+ */
44
+ export const useResponsiveValue = <T>(value: ResponsiveValue<T>): T =>
45
+ resolveResponsiveValue(value, useBreakpoint());
46
+
47
+ /**
48
+ * Wraps a component so that the specified props accept `ResponsiveValue<T>`.
49
+ * The wrapper resolves each responsive prop to a plain value before rendering,
50
+ * so the wrapped component stays completely unaware of breakpoints.
51
+ */
52
+ export function withResponsiveProps<Props, const Keys extends readonly (keyof Props)[]>(
53
+ Component: FunctionComponent<Props>,
54
+ responsiveKeys: Keys,
55
+ ) {
56
+ type EnhancedProps = {
57
+ [P in keyof Props]: P extends Keys[number] ? ResponsiveValue<NonNullable<Props[P]>> : Props[P];
58
+ };
59
+
60
+ const Wrapper = (props: EnhancedProps) => {
61
+ const breakpoint = useBreakpoint();
62
+ const resolved = { ...props } as Record<string, unknown>;
63
+
64
+ for (const key of responsiveKeys) {
65
+ const k = String(key);
66
+ const value = resolved[k];
67
+
68
+ if (value !== undefined) {
69
+ resolved[k] = resolveResponsiveValue(value, breakpoint);
70
+ }
71
+ }
72
+
73
+ return createElement(Component as FunctionComponent<Record<string, unknown>>, resolved);
74
+ };
75
+
76
+ Wrapper.displayName = `withResponsiveProps(${Component.displayName ?? Component.name})`;
77
+
78
+ return Wrapper;
79
+ }
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { BREAKPOINT_LG, isResponsiveValue, resolveResponsiveValue } from './responsive';
4
+
5
+ describe('BREAKPOINT_LG', () => {
6
+ it('should be sourced from the SCSS variable and equal 1440', () => {
7
+ expect(BREAKPOINT_LG).toBe(1440);
8
+ });
9
+ });
10
+
11
+ describe('isResponsiveValue', () => {
12
+ it('should return true for a responsive object', () => {
13
+ expect(isResponsiveValue({ lg: 'large', md: 'small' })).toBe(true);
14
+ });
15
+
16
+ it('should return false for a string', () => {
17
+ expect(isResponsiveValue('large')).toBe(false);
18
+ });
19
+
20
+ it('should return false for null', () => {
21
+ expect(isResponsiveValue(null as unknown as string)).toBe(false);
22
+ });
23
+
24
+ it('should return true for an object with only lg', () => {
25
+ expect(isResponsiveValue({ lg: 'large' })).toBe(true);
26
+ });
27
+
28
+ it('should return true for an object with only md', () => {
29
+ expect(isResponsiveValue({ md: 'small' })).toBe(true);
30
+ });
31
+ });
32
+
33
+ describe('resolveResponsiveValue', () => {
34
+ it('should return static value unchanged for both breakpoints', () => {
35
+ expect(resolveResponsiveValue('large', 'lg')).toBe('large');
36
+ expect(resolveResponsiveValue('large', 'md')).toBe('large');
37
+ });
38
+
39
+ it('should resolve lg value from responsive object', () => {
40
+ expect(resolveResponsiveValue({ lg: 'large', md: 'small' }, 'lg')).toBe('large');
41
+ });
42
+
43
+ it('should resolve md value from responsive object', () => {
44
+ expect(resolveResponsiveValue({ lg: 'large', md: 'small' }, 'md')).toBe('small');
45
+ });
46
+
47
+ it('should fall back to lg when md is not specified', () => {
48
+ expect(resolveResponsiveValue({ lg: 'large' }, 'md')).toBe('large');
49
+ });
50
+
51
+ it('should fall back to md when lg is not specified', () => {
52
+ expect(resolveResponsiveValue({ md: 'small' }, 'lg')).toBe('small');
53
+ });
54
+ });
@@ -0,0 +1 @@
1
+ export type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
@@ -13,8 +13,8 @@ describe('useControlled', () => {
13
13
  });
14
14
 
15
15
  it('should call the provided setValue', async () => {
16
- const setValue = vi.fn();
17
- const { result, act } = await renderHook(() => useControlled<string>('hello', setValue, ''));
16
+ const setValue = vi.fn(() => {});
17
+ const { result, act } = await renderHook(() => useControlled('hello', setValue, ''));
18
18
 
19
19
  await act(() => {
20
20
  result.current[1]('world');
@@ -33,13 +33,13 @@ describe('useControlled', () => {
33
33
 
34
34
  describe('uncontrolled mode', () => {
35
35
  it('should use defaultValue as the initial value', async () => {
36
- const { result } = await renderHook(() => useControlled<string>(undefined, undefined, 'fallback'));
36
+ const { result } = await renderHook(() => useControlled(undefined, undefined, 'fallback'));
37
37
 
38
38
  expect(result.current[0]).toBe('fallback');
39
39
  });
40
40
 
41
41
  it('should update internal state via the setter', async () => {
42
- const { result, act } = await renderHook(() => useControlled<string>(undefined, undefined, 'fallback'));
42
+ const { result, act } = await renderHook(() => useControlled(undefined, undefined, 'fallback'));
43
43
 
44
44
  expect(result.current[0]).toBe('fallback');
45
45
 
@@ -63,8 +63,8 @@ describe('useControlled', () => {
63
63
  });
64
64
 
65
65
  it('should treat setValue-only (no value) as uncontrolled', async () => {
66
- const setValue = vi.fn();
67
- const { result, act } = await renderHook(() => useControlled<string>(undefined, setValue, 'default'));
66
+ const setValue = vi.fn(() => {});
67
+ const { result, act } = await renderHook(() => useControlled(undefined, setValue, 'default'));
68
68
 
69
69
  expect(result.current[0]).toBe('default');
70
70
 
@@ -10,7 +10,7 @@ export const useControlled = <T>(
10
10
  defaultValue: T,
11
11
  ) => {
12
12
  const isControlled = value !== undefined && setValue !== undefined;
13
- const [internalValue, setInternalValue] = useState<T>(value ?? defaultValue);
13
+ const [internalValue, setInternalValue] = useState(value ?? defaultValue);
14
14
  const resolvedValue = isControlled ? value : internalValue;
15
15
  const resolvedSetValue = isControlled ? setValue : setInternalValue;
16
16