@drivenets/design-system 0.7.3 → 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 (117) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/index.cjs +5730 -4569
  3. package/dist/index.d.cts +1768 -1065
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1768 -1065
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +5718 -4577
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +14 -14
  11. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +1 -2
  12. package/src/components/ds-autocomplete/ds-autocomplete.tsx +13 -8
  13. package/src/components/ds-autocomplete/ds-autocomplete.types.ts +2 -1
  14. package/src/components/ds-button/versions/ds-button-new/ds-button-new.module.scss +25 -9
  15. package/src/components/ds-button/versions/ds-button-new/ds-button-new.tsx +5 -5
  16. package/src/components/ds-button/versions/ds-button-new/ds-button-new.types.ts +11 -1
  17. package/src/components/ds-button/versions/ds-button-new/index.ts +4 -1
  18. package/src/components/ds-button-v3/ds-button-v3.module.scss +391 -0
  19. package/src/components/ds-button-v3/ds-button-v3.tsx +55 -0
  20. package/src/components/ds-button-v3/ds-button-v3.types.ts +49 -0
  21. package/src/components/ds-button-v3/index.ts +2 -0
  22. package/src/components/ds-comment-bubble/ds-comment-bubble.module.scss +6 -2
  23. package/src/components/ds-comment-bubble/ds-comment-bubble.tsx +22 -15
  24. package/src/components/ds-comment-bubble/ds-comment-bubble.types.ts +1 -0
  25. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.tsx +1 -1
  26. package/src/components/ds-comments-drawer/ds-comments-drawer.tsx +3 -9
  27. package/src/components/ds-date-input/ds-date-input.tsx +3 -4
  28. package/src/components/ds-date-input/ds-date-input.types.ts +3 -1
  29. package/src/components/ds-date-input/index.ts +10 -0
  30. package/src/components/ds-date-picker/components/calendar-table.module.scss +93 -0
  31. package/src/components/ds-date-picker/components/calendar-view-control/calendar-view-control.module.scss +48 -0
  32. package/src/components/ds-date-picker/components/calendar-view-control/calendar-view-control.tsx +20 -0
  33. package/src/components/ds-date-picker/components/calendar-view-control/index.ts +1 -0
  34. package/src/components/ds-date-picker/components/day-view/day-view.module.scss +12 -0
  35. package/src/components/ds-date-picker/components/day-view/day-view.tsx +45 -0
  36. package/src/components/ds-date-picker/components/day-view/index.ts +1 -0
  37. package/src/components/ds-date-picker/components/month-view/index.ts +1 -0
  38. package/src/components/ds-date-picker/components/month-view/month-view.module.scss +4 -0
  39. package/src/components/ds-date-picker/components/month-view/month-view.tsx +35 -0
  40. package/src/components/ds-date-picker/components/year-view/index.ts +1 -0
  41. package/src/components/ds-date-picker/components/year-view/year-view.module.scss +4 -0
  42. package/src/components/ds-date-picker/components/year-view/year-view.tsx +35 -0
  43. package/src/components/ds-date-picker/ds-date-picker.module.scss +29 -0
  44. package/src/components/ds-date-picker/ds-date-picker.tsx +211 -0
  45. package/src/components/ds-date-picker/ds-date-picker.types.ts +94 -0
  46. package/src/components/ds-date-picker/ds-date-picker.utils.ts +87 -0
  47. package/src/components/ds-date-picker/ds-date-picker.utils.unit.test.ts +226 -0
  48. package/src/components/ds-date-picker/index.ts +2 -0
  49. package/src/components/ds-date-range-picker/ds-date-range-picker.module.scss +20 -0
  50. package/src/components/ds-date-range-picker/ds-date-range-picker.tsx +101 -0
  51. package/src/components/ds-date-range-picker/ds-date-range-picker.types.ts +95 -0
  52. package/src/components/ds-date-range-picker/ds-date-range-picker.utils.ts +19 -0
  53. package/src/components/ds-date-range-picker/ds-date-range-picker.utils.unit.test.ts +48 -0
  54. package/src/components/ds-date-range-picker/index.ts +9 -0
  55. package/src/components/ds-drawer/ds-drawer.tsx +6 -9
  56. package/src/components/ds-drawer/ds-drawer.types.ts +4 -2
  57. package/src/components/ds-file-upload/ds-file-upload-api.types.ts +1 -0
  58. package/src/components/ds-file-upload/hooks/use-file-upload.ts +1 -1
  59. package/src/components/ds-file-upload/index.ts +6 -1
  60. package/src/components/ds-file-upload/utils/file-factory.ts +1 -0
  61. package/src/components/ds-file-upload/utils/file-factory.unit.test.ts +37 -0
  62. package/src/components/ds-form-control/ds-form-control.tsx +7 -3
  63. package/src/components/ds-key-value-pair/ds-key-value-pair.module.scss +72 -0
  64. package/src/components/ds-key-value-pair/ds-key-value-pair.tsx +47 -0
  65. package/src/components/ds-key-value-pair/ds-key-value-pair.types.ts +24 -0
  66. package/src/components/ds-key-value-pair/index.ts +3 -0
  67. package/src/components/ds-loader/ds-loader.tsx +1 -1
  68. package/src/components/ds-panel/ds-panel.module.scss +4 -1
  69. package/src/components/ds-panel/ds-panel.tsx +8 -3
  70. package/src/components/ds-panel/ds-panel.types.ts +8 -1
  71. package/src/components/ds-panel/index.ts +4 -1
  72. package/src/components/ds-select/ds-select.module.scss +4 -1
  73. package/src/components/ds-split-button/ds-split-button.module.scss +99 -0
  74. package/src/components/ds-split-button/ds-split-button.tsx +49 -0
  75. package/src/components/ds-split-button/ds-split-button.types.ts +25 -0
  76. package/src/components/ds-split-button/index.ts +10 -0
  77. package/src/components/ds-stack/ds-stack.module.scss +4 -0
  78. package/src/components/ds-stack/ds-stack.tsx +35 -0
  79. package/src/components/ds-stack/ds-stack.types.ts +19 -0
  80. package/src/components/ds-stack/index.ts +18 -0
  81. package/src/components/ds-status-badge/ds-status-badge.module.scss +1 -1
  82. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +6 -4
  83. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +5 -8
  84. package/src/components/ds-table/ds-table.module.scss +6 -0
  85. package/src/components/ds-table/ds-table.tsx +3 -0
  86. package/src/components/ds-time-picker/components/time-item/index.ts +1 -0
  87. package/src/components/ds-time-picker/components/time-item/time-item.module.scss +37 -0
  88. package/src/components/ds-time-picker/components/time-item/time-item.tsx +26 -0
  89. package/src/components/ds-time-picker/components/time-item/time-item.types.ts +10 -0
  90. package/src/components/ds-time-picker/components/time-scroller/index.ts +2 -0
  91. package/src/components/ds-time-picker/components/time-scroller/time-scroller.module.scss +25 -0
  92. package/src/components/ds-time-picker/components/time-scroller/time-scroller.tsx +61 -0
  93. package/src/components/ds-time-picker/components/time-scroller/time-scroller.types.ts +17 -0
  94. package/src/components/ds-time-picker/components/time-scroller/time-scroller.utils.ts +44 -0
  95. package/src/components/ds-time-picker/ds-time-picker.module.scss +11 -0
  96. package/src/components/ds-time-picker/ds-time-picker.tsx +178 -0
  97. package/src/components/ds-time-picker/ds-time-picker.types.ts +54 -0
  98. package/src/components/ds-time-picker/ds-time-picker.utils.ts +176 -0
  99. package/src/components/ds-time-picker/ds-time-picker.utils.unit.test.ts +316 -0
  100. package/src/components/ds-time-picker/index.ts +3 -0
  101. package/src/components/ds-tooltip/ds-tooltip.module.scss +1 -7
  102. package/src/components/ds-tooltip/ds-tooltip.tsx +9 -9
  103. package/src/components/ds-tooltip/ds-tooltip.types.ts +7 -1
  104. package/src/index.ts +8 -0
  105. package/src/styles/_breakpoints.module.scss +5 -0
  106. package/src/styles/_responsive.scss +13 -0
  107. package/src/styles/_root_updated.scss +593 -0
  108. package/src/styles/_variables.scss +2 -0
  109. package/src/styles/shared/_button.scss +2 -0
  110. package/src/styles/shared/_dropdown.scss +4 -1
  111. package/src/styles/shared/_input.scss +3 -40
  112. package/src/utils/responsive.browser.test.ts +94 -0
  113. package/src/utils/responsive.ts +79 -0
  114. package/src/utils/responsive.unit.test.ts +54 -0
  115. package/src/utils/type-utils.ts +1 -0
  116. package/src/utils/use-controlled.browser.test.ts +79 -0
  117. package/src/utils/use-controlled.ts +18 -0
@@ -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;
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { renderHook } from 'vitest-browser-react';
3
+
4
+ import { useControlled } from './use-controlled';
5
+
6
+ describe('useControlled', () => {
7
+ describe('controlled mode', () => {
8
+ it('should return the controlled value', async () => {
9
+ const setValue = vi.fn();
10
+ const { result } = await renderHook(() => useControlled('hello', setValue, undefined));
11
+
12
+ expect(result.current[0]).toBe('hello');
13
+ });
14
+
15
+ it('should call the provided setValue', async () => {
16
+ const setValue = vi.fn(() => {});
17
+ const { result, act } = await renderHook(() => useControlled('hello', setValue, ''));
18
+
19
+ await act(() => {
20
+ result.current[1]('world');
21
+ });
22
+
23
+ expect(setValue).toHaveBeenCalledWith('world');
24
+ });
25
+
26
+ it('should ignore the defaultValue when controlled', async () => {
27
+ const setValue = vi.fn();
28
+ const { result } = await renderHook(() => useControlled('controlled', setValue, 'default'));
29
+
30
+ expect(result.current[0]).toBe('controlled');
31
+ });
32
+ });
33
+
34
+ describe('uncontrolled mode', () => {
35
+ it('should use defaultValue as the initial value', async () => {
36
+ const { result } = await renderHook(() => useControlled(undefined, undefined, 'fallback'));
37
+
38
+ expect(result.current[0]).toBe('fallback');
39
+ });
40
+
41
+ it('should update internal state via the setter', async () => {
42
+ const { result, act } = await renderHook(() => useControlled(undefined, undefined, 'fallback'));
43
+
44
+ expect(result.current[0]).toBe('fallback');
45
+
46
+ await act(() => {
47
+ result.current[1]('updated');
48
+ });
49
+
50
+ expect(result.current[0]).toBe('updated');
51
+ });
52
+ });
53
+
54
+ describe('edge cases', () => {
55
+ it('should treat value-only (no setValue) as uncontrolled', async () => {
56
+ const { result, act } = await renderHook(() => useControlled('initial', undefined, undefined));
57
+
58
+ await act(() => {
59
+ result.current[1]('updated');
60
+ });
61
+
62
+ expect(result.current[0]).toBe('updated');
63
+ });
64
+
65
+ it('should treat setValue-only (no value) as uncontrolled', async () => {
66
+ const setValue = vi.fn(() => {});
67
+ const { result, act } = await renderHook(() => useControlled(undefined, setValue, 'default'));
68
+
69
+ expect(result.current[0]).toBe('default');
70
+
71
+ await act(() => {
72
+ result.current[1]('updated');
73
+ });
74
+
75
+ expect(result.current[0]).toBe('updated');
76
+ expect(setValue).not.toHaveBeenCalled();
77
+ });
78
+ });
79
+ });
@@ -0,0 +1,18 @@
1
+ import { useState } from 'react';
2
+
3
+ /**
4
+ * A hook that manages a controlled or uncontrolled value.
5
+ * It is considered controlled if both value and setValue are provided.
6
+ */
7
+ export const useControlled = <T>(
8
+ value: T | undefined,
9
+ setValue: ((value: T) => void) | undefined,
10
+ defaultValue: T,
11
+ ) => {
12
+ const isControlled = value !== undefined && setValue !== undefined;
13
+ const [internalValue, setInternalValue] = useState(value ?? defaultValue);
14
+ const resolvedValue = isControlled ? value : internalValue;
15
+ const resolvedSetValue = isControlled ? setValue : setInternalValue;
16
+
17
+ return [resolvedValue, resolvedSetValue] as const;
18
+ };