@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
@@ -0,0 +1,178 @@
1
+ import { type ChangeEvent, Fragment, useEffect, useRef, useState } from 'react';
2
+ import { Popover } from '@ark-ui/react/popover';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import classNames from 'classnames';
5
+ import type { DsTimePickerProps } from './ds-time-picker.types';
6
+ import { clampTime, formatTime, parseTime, timeScrollerAdapter } from './ds-time-picker.utils';
7
+ import { TimeScroller } from './components/time-scroller/time-scroller';
8
+ import { DsIcon } from '../ds-icon';
9
+ import { DsButton } from '../ds-button';
10
+ import { DsTextInput } from '../ds-text-input';
11
+ import { useControlled } from '../../utils/use-controlled';
12
+ import styles from './ds-time-picker.module.scss';
13
+
14
+ const DsTimePicker = (props: DsTimePickerProps) => {
15
+ const {
16
+ id,
17
+ ref,
18
+ className,
19
+ placeholder,
20
+ min,
21
+ max,
22
+ disabled,
23
+ readOnly,
24
+ hideClearButton = false,
25
+ disablePortal = false,
26
+ locale,
27
+ slotProps,
28
+ } = props;
29
+ const [value, setValue] = useControlled(props.value, props.onChange, props.defaultValue ?? null);
30
+ const [isOpen, setIsOpen] = useControlled(props.open, props.onOpenChange, props.defaultOpen ?? false);
31
+ const [isFocused, setIsFocused] = useState(false);
32
+
33
+ const inputRef = useRef<HTMLInputElement>(null);
34
+
35
+ const formattedValue = formatTime(value);
36
+
37
+ // If the time is before the min or after the max, immediately clamp it to the min or max
38
+ if (value) {
39
+ const clamped = clampTime(value, min, max);
40
+
41
+ if (clamped !== value) {
42
+ setValue(clamped);
43
+ }
44
+ }
45
+
46
+ // Reformat on value change, but skip while focused to avoid disrupting typing
47
+ useEffect(() => {
48
+ if (!isFocused && inputRef.current) {
49
+ inputRef.current.value = formattedValue;
50
+ }
51
+ }, [isFocused, formattedValue]);
52
+
53
+ const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
54
+ const inputVal = e.target.value;
55
+
56
+ if (!inputVal) {
57
+ setValue(null);
58
+ return;
59
+ }
60
+
61
+ const parsed = parseTime(inputVal);
62
+
63
+ if (parsed) {
64
+ const newDate = value ? new Date(value) : new Date();
65
+ newDate.setHours(parsed.hours, parsed.minutes, 0, 0);
66
+ setValue(newDate);
67
+ }
68
+ };
69
+
70
+ const handleFocus = () => {
71
+ setIsFocused(true);
72
+ };
73
+
74
+ const resetInput = () => {
75
+ if (inputRef.current) {
76
+ inputRef.current.value = value ? formattedValue : '';
77
+ }
78
+ };
79
+
80
+ const handleBlur = () => {
81
+ setIsFocused(false);
82
+
83
+ const inputVal = inputRef.current?.value ?? '';
84
+
85
+ if (!inputVal) {
86
+ setValue(null);
87
+ return;
88
+ }
89
+
90
+ const parsed = parseTime(inputVal);
91
+
92
+ if (parsed) {
93
+ const newDate = value ? new Date(value) : new Date();
94
+ newDate.setHours(parsed.hours, parsed.minutes, 0, 0);
95
+ setValue(newDate);
96
+ } else {
97
+ resetInput();
98
+ }
99
+ };
100
+
101
+ const handleOpenChange = (details: Popover.OpenChangeDetails) => {
102
+ if (disabled || readOnly) {
103
+ return;
104
+ }
105
+
106
+ setIsOpen(details.open);
107
+ };
108
+
109
+ const showClearButton = !hideClearButton && !disabled && !readOnly && !!value;
110
+
111
+ const Wrapper = disablePortal ? Fragment : Portal;
112
+
113
+ return (
114
+ <Popover.Root
115
+ open={isOpen}
116
+ onOpenChange={handleOpenChange}
117
+ positioning={{ placement: 'bottom-start', gutter: 4 }}
118
+ >
119
+ <Popover.Anchor asChild>
120
+ <div ref={ref} className={classNames(styles.root, className)}>
121
+ <DsTextInput
122
+ ref={inputRef}
123
+ id={id}
124
+ placeholder={placeholder ?? 'hh:mm AM/PM'}
125
+ defaultValue={formattedValue}
126
+ onChange={handleInputChange}
127
+ onFocus={handleFocus}
128
+ onBlur={handleBlur}
129
+ readOnly={readOnly}
130
+ disabled={disabled}
131
+ {...slotProps?.input}
132
+ slots={{
133
+ endAdornment: slotProps?.input?.slots?.endAdornment ?? (
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
+ </>
160
+ ),
161
+ ...slotProps?.input?.slots,
162
+ }}
163
+ />
164
+ </div>
165
+ </Popover.Anchor>
166
+
167
+ <Wrapper>
168
+ <Popover.Positioner>
169
+ <Popover.Content {...slotProps?.popover}>
170
+ <TimeScroller open={isOpen} {...timeScrollerAdapter(value, setValue, min, max)} />
171
+ </Popover.Content>
172
+ </Popover.Positioner>
173
+ </Wrapper>
174
+ </Popover.Root>
175
+ );
176
+ };
177
+
178
+ export default DsTimePicker;
@@ -0,0 +1,54 @@
1
+ import type { RefObject } from 'react';
2
+ import type { DsTextInputProps } from '../ds-text-input';
3
+ import type { PopoverContentProps } from '@ark-ui/react/popover';
4
+
5
+ export interface DsTimePickerProps {
6
+ id?: string;
7
+ ref?: RefObject<HTMLDivElement>;
8
+ className?: string;
9
+
10
+ value?: Date | null;
11
+ defaultValue?: Date;
12
+
13
+ open?: boolean;
14
+ defaultOpen?: boolean;
15
+
16
+ placeholder?: string;
17
+
18
+ min?: Date;
19
+ max?: Date;
20
+
21
+ disabled?: boolean;
22
+ readOnly?: boolean;
23
+
24
+ /**
25
+ * Whether to hide the clear button
26
+ * @default false
27
+ */
28
+ hideClearButton?: boolean;
29
+
30
+ /**
31
+ * Whether to disable the portal for the popover content
32
+ * @default false
33
+ */
34
+ disablePortal?: boolean;
35
+
36
+ locale?: DsTimePickerLocale;
37
+ slotProps?: DsTimePickerSlotProps;
38
+
39
+ onChange?: (value: Date | null) => void;
40
+ onOpenChange?: (open: boolean) => void;
41
+ }
42
+
43
+ export interface DsTimePickerLocale {
44
+ openLabel?: string;
45
+ clearLabel?: string;
46
+ hourLabel?: string;
47
+ minuteLabel?: string;
48
+ periodLabel?: string;
49
+ }
50
+
51
+ export interface DsTimePickerSlotProps {
52
+ input?: DsTextInputProps;
53
+ popover?: PopoverContentProps;
54
+ }
@@ -0,0 +1,176 @@
1
+ import type { TimePeriod, TimeScrollerProps } from './components/time-scroller';
2
+
3
+ const AM_END = 11 * 60 + 59;
4
+ const PM_START = 12 * 60;
5
+ const PM_END = 23 * 60 + 59;
6
+
7
+ const to24Hour = (hour12: number, period: TimePeriod): number => {
8
+ if (period === 'AM') {
9
+ return hour12 === 12 ? 0 : hour12;
10
+ }
11
+
12
+ return hour12 === 12 ? 12 : hour12 + 12;
13
+ };
14
+
15
+ const to12Hour = (hour24: number): number => {
16
+ if (hour24 === 0) {
17
+ return 12;
18
+ }
19
+
20
+ return hour24 > 12 ? hour24 - 12 : hour24;
21
+ };
22
+
23
+ const toMinutesSinceMidnight = (date: Date): number => {
24
+ return date.getHours() * 60 + date.getMinutes();
25
+ };
26
+
27
+ /**
28
+ * If the time is before the min or after the max, clamp it to the min or max
29
+ */
30
+ export const clampTime = (date: Date, min?: Date, max?: Date): Date => {
31
+ const time = toMinutesSinceMidnight(date);
32
+
33
+ if (min && time < toMinutesSinceMidnight(min)) {
34
+ const clamped = new Date(date);
35
+ clamped.setHours(min.getHours(), min.getMinutes(), 0, 0);
36
+ return clamped;
37
+ }
38
+
39
+ if (max && time > toMinutesSinceMidnight(max)) {
40
+ const clamped = new Date(date);
41
+ clamped.setHours(max.getHours(), max.getMinutes(), 0, 0);
42
+ return clamped;
43
+ }
44
+
45
+ return date;
46
+ };
47
+
48
+ const getTimeRange = (min?: Date, max?: Date) => ({
49
+ minTime: min ? toMinutesSinceMidnight(min) : 0,
50
+ maxTime: max ? toMinutesSinceMidnight(max) : PM_END,
51
+ });
52
+
53
+ const validateMinMax = (hour?: number, period?: TimePeriod, min?: Date, max?: Date) => {
54
+ if (!min && !max) {
55
+ return {};
56
+ }
57
+
58
+ const { minTime, maxTime } = getTimeRange(min, max);
59
+
60
+ return {
61
+ isPeriodDisabled: (p: TimePeriod) => {
62
+ if (p === 'AM') {
63
+ return AM_END < minTime;
64
+ }
65
+
66
+ return PM_END < minTime || PM_START > maxTime;
67
+ },
68
+
69
+ isHourDisabled: (h: number) => {
70
+ if (!period) {
71
+ return false;
72
+ }
73
+
74
+ const h24 = to24Hour(h, period);
75
+ return h24 * 60 + 59 < minTime || h24 * 60 > maxTime;
76
+ },
77
+
78
+ isMinuteDisabled: (m: number) => {
79
+ if (hour === undefined || !period) {
80
+ return false;
81
+ }
82
+
83
+ const time = to24Hour(hour, period) * 60 + m;
84
+ return time < minTime || time > maxTime;
85
+ },
86
+ };
87
+ };
88
+
89
+ /**
90
+ * Format a Date to a 12-hour time string (hh:mm AM/PM)
91
+ */
92
+ export const formatTime = (date?: Date | null): string => {
93
+ if (!date) {
94
+ return '';
95
+ }
96
+
97
+ const hour24 = date.getHours();
98
+ const minute = date.getMinutes();
99
+ const period = hour24 >= 12 ? 'PM' : 'AM';
100
+ const paddedHour = String(to12Hour(hour24)).padStart(2, '0');
101
+ const paddedMinute = String(minute).padStart(2, '0');
102
+
103
+ return `${paddedHour}:${paddedMinute} ${period}`;
104
+ };
105
+
106
+ /**
107
+ * Parse a 12-hour time string (hh:mm AM/PM) to an object with hours and minutes
108
+ */
109
+ export const parseTime = (text: string) => {
110
+ const match = text.match(/^(\d{1,2}):(\d{2})\s+(AM|PM)$/i);
111
+
112
+ if (!match?.[1] || !match[2] || !match[3]) {
113
+ return null;
114
+ }
115
+
116
+ const h12 = parseInt(match[1], 10);
117
+ const minutes = parseInt(match[2], 10);
118
+ const period = match[3].toUpperCase();
119
+
120
+ if (h12 < 1 || h12 > 12 || minutes < 0 || minutes > 59) {
121
+ return null;
122
+ }
123
+
124
+ const hours = period === 'AM' ? (h12 === 12 ? 0 : h12) : h12 === 12 ? 12 : h12 + 12;
125
+
126
+ return { hours, minutes };
127
+ };
128
+
129
+ export const timeScrollerAdapter = (
130
+ date?: Date | null,
131
+ setDate?: (date: Date | null) => void,
132
+ min?: Date,
133
+ max?: Date,
134
+ ) => {
135
+ const hour = date ? to12Hour(date.getHours()) : undefined;
136
+ const minute = date?.getMinutes();
137
+ const period = date ? (date.getHours() >= 12 ? 'PM' : 'AM') : undefined;
138
+
139
+ const clampAndSet = (newDate: Date) => {
140
+ setDate?.(clampTime(newDate, min, max));
141
+ };
142
+
143
+ const { isHourDisabled, isMinuteDisabled, isPeriodDisabled } = validateMinMax(hour, period, min, max);
144
+
145
+ return {
146
+ slotProps: {
147
+ hour: {
148
+ value: hour,
149
+ onChange: (h: number) => {
150
+ const newDate = date ? new Date(date) : new Date();
151
+ newDate.setHours(to24Hour(h, period ?? 'AM'), minute ?? 0, 0, 0);
152
+ clampAndSet(newDate);
153
+ },
154
+ isDisabled: isHourDisabled,
155
+ },
156
+ minute: {
157
+ value: minute,
158
+ onChange: (m: number) => {
159
+ const newDate = date ? new Date(date) : new Date();
160
+ newDate.setHours(to24Hour(hour ?? 12, period ?? 'AM'), m, 0, 0);
161
+ clampAndSet(newDate);
162
+ },
163
+ isDisabled: isMinuteDisabled,
164
+ },
165
+ period: {
166
+ value: period,
167
+ onChange: (p: TimePeriod) => {
168
+ const newDate = date ? new Date(date) : new Date();
169
+ newDate.setHours(to24Hour(hour ?? 12, p), minute ?? 0, 0, 0);
170
+ clampAndSet(newDate);
171
+ },
172
+ isDisabled: isPeriodDisabled,
173
+ },
174
+ },
175
+ } satisfies Pick<TimeScrollerProps, 'slotProps'>;
176
+ };