@drivenets/design-system 0.7.3 → 0.8.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 (79) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/index.cjs +4445 -3769
  3. package/dist/index.d.cts +1224 -945
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1224 -945
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4446 -3776
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +7 -8
  11. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +1 -1
  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-comment-bubble/ds-comment-bubble.module.scss +6 -2
  15. package/src/components/ds-comment-bubble/ds-comment-bubble.tsx +22 -15
  16. package/src/components/ds-comment-bubble/ds-comment-bubble.types.ts +1 -0
  17. package/src/components/ds-date-input/ds-date-input.tsx +3 -4
  18. package/src/components/ds-date-input/ds-date-input.types.ts +3 -1
  19. package/src/components/ds-date-input/index.ts +10 -0
  20. package/src/components/ds-date-picker/components/calendar-table.module.scss +93 -0
  21. package/src/components/ds-date-picker/components/calendar-view-control/calendar-view-control.module.scss +48 -0
  22. package/src/components/ds-date-picker/components/calendar-view-control/calendar-view-control.tsx +20 -0
  23. package/src/components/ds-date-picker/components/calendar-view-control/index.ts +1 -0
  24. package/src/components/ds-date-picker/components/day-view/day-view.module.scss +12 -0
  25. package/src/components/ds-date-picker/components/day-view/day-view.tsx +45 -0
  26. package/src/components/ds-date-picker/components/day-view/index.ts +1 -0
  27. package/src/components/ds-date-picker/components/month-view/index.ts +1 -0
  28. package/src/components/ds-date-picker/components/month-view/month-view.module.scss +4 -0
  29. package/src/components/ds-date-picker/components/month-view/month-view.tsx +35 -0
  30. package/src/components/ds-date-picker/components/year-view/index.ts +1 -0
  31. package/src/components/ds-date-picker/components/year-view/year-view.module.scss +4 -0
  32. package/src/components/ds-date-picker/components/year-view/year-view.tsx +35 -0
  33. package/src/components/ds-date-picker/ds-date-picker.module.scss +19 -0
  34. package/src/components/ds-date-picker/ds-date-picker.tsx +209 -0
  35. package/src/components/ds-date-picker/ds-date-picker.types.ts +94 -0
  36. package/src/components/ds-date-picker/ds-date-picker.utils.ts +87 -0
  37. package/src/components/ds-date-picker/ds-date-picker.utils.unit.test.ts +226 -0
  38. package/src/components/ds-date-picker/index.ts +2 -0
  39. package/src/components/ds-date-range-picker/ds-date-range-picker.module.scss +20 -0
  40. package/src/components/ds-date-range-picker/ds-date-range-picker.tsx +101 -0
  41. package/src/components/ds-date-range-picker/ds-date-range-picker.types.ts +95 -0
  42. package/src/components/ds-date-range-picker/ds-date-range-picker.utils.ts +19 -0
  43. package/src/components/ds-date-range-picker/ds-date-range-picker.utils.unit.test.ts +48 -0
  44. package/src/components/ds-date-range-picker/index.ts +9 -0
  45. package/src/components/ds-file-upload/ds-file-upload-api.types.ts +1 -0
  46. package/src/components/ds-file-upload/hooks/use-file-upload.ts +1 -1
  47. package/src/components/ds-file-upload/index.ts +6 -1
  48. package/src/components/ds-file-upload/utils/file-factory.ts +1 -0
  49. package/src/components/ds-file-upload/utils/file-factory.unit.test.ts +37 -0
  50. package/src/components/ds-form-control/ds-form-control.tsx +5 -0
  51. package/src/components/ds-key-value-pair/ds-key-value-pair.module.scss +72 -0
  52. package/src/components/ds-key-value-pair/ds-key-value-pair.tsx +47 -0
  53. package/src/components/ds-key-value-pair/ds-key-value-pair.types.ts +24 -0
  54. package/src/components/ds-key-value-pair/index.ts +3 -0
  55. package/src/components/ds-loader/ds-loader.tsx +1 -1
  56. package/src/components/ds-status-badge/ds-status-badge.module.scss +1 -1
  57. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +4 -2
  58. package/src/components/ds-time-picker/components/time-item/index.ts +1 -0
  59. package/src/components/ds-time-picker/components/time-item/time-item.module.scss +37 -0
  60. package/src/components/ds-time-picker/components/time-item/time-item.tsx +26 -0
  61. package/src/components/ds-time-picker/components/time-item/time-item.types.ts +10 -0
  62. package/src/components/ds-time-picker/components/time-scroller/index.ts +2 -0
  63. package/src/components/ds-time-picker/components/time-scroller/time-scroller.module.scss +25 -0
  64. package/src/components/ds-time-picker/components/time-scroller/time-scroller.tsx +61 -0
  65. package/src/components/ds-time-picker/components/time-scroller/time-scroller.types.ts +17 -0
  66. package/src/components/ds-time-picker/components/time-scroller/time-scroller.utils.ts +44 -0
  67. package/src/components/ds-time-picker/ds-time-picker.tsx +158 -0
  68. package/src/components/ds-time-picker/ds-time-picker.types.ts +48 -0
  69. package/src/components/ds-time-picker/ds-time-picker.utils.ts +176 -0
  70. package/src/components/ds-time-picker/ds-time-picker.utils.unit.test.ts +316 -0
  71. package/src/components/ds-time-picker/index.ts +3 -0
  72. package/src/components/ds-tooltip/ds-tooltip.module.scss +1 -7
  73. package/src/components/ds-tooltip/ds-tooltip.tsx +9 -9
  74. package/src/components/ds-tooltip/ds-tooltip.types.ts +7 -1
  75. package/src/index.ts +4 -0
  76. package/src/styles/_root_updated.scss +593 -0
  77. package/src/styles/shared/_dropdown.scss +4 -1
  78. package/src/utils/use-controlled.browser.test.ts +79 -0
  79. package/src/utils/use-controlled.ts +18 -0
@@ -0,0 +1,101 @@
1
+ import classNames from 'classnames';
2
+ import { DsFormControl } from '../ds-form-control';
3
+ import { DsButton } from '../ds-button';
4
+ import { DsIcon } from '../ds-icon';
5
+ import { useControlled } from '../../utils/use-controlled';
6
+ import type { DateRangeValue, DsDateRangePickerProps } from './ds-date-range-picker.types';
7
+ import styles from './ds-date-range-picker.module.scss';
8
+ import { earlierDate, laterDate } from './ds-date-range-picker.utils';
9
+
10
+ const DsDateRangePicker = ({
11
+ value: valueProp,
12
+ defaultValue = [null, null],
13
+ onChange,
14
+ min,
15
+ max,
16
+ withTime = false,
17
+ orientation = 'horizontal',
18
+ hideClearAll = false,
19
+ closeOnSelect = true,
20
+ disabled,
21
+ readOnly,
22
+ className,
23
+ disablePortal,
24
+ locale,
25
+ slotProps,
26
+ }: DsDateRangePickerProps) => {
27
+ const [value, setValue] = useControlled<DateRangeValue>(valueProp, onChange, defaultValue);
28
+
29
+ const [startDate, endDate] = value;
30
+
31
+ const handleStartChange = (date: Date | null) => {
32
+ setValue([date, endDate]);
33
+ };
34
+
35
+ const handleEndChange = (date: Date | null) => {
36
+ setValue([startDate, date]);
37
+ };
38
+
39
+ const handleClearAll = () => {
40
+ setValue([null, null]);
41
+ };
42
+
43
+ // max allowed date for the "Start date" is the earliest of "max" and the selected "endDate"
44
+ const effectiveStartMax = earlierDate(max, endDate ?? undefined);
45
+
46
+ // min allowed date for the "End date" is the later of "min" and the selected "startDate"
47
+ const effectiveEndMin = laterDate(min, startDate ?? undefined);
48
+
49
+ const showClearAll = !hideClearAll && !disabled && !readOnly && (!!startDate || !!endDate);
50
+
51
+ return (
52
+ <div className={classNames(styles.root, styles[orientation], className)}>
53
+ <DsFormControl
54
+ label="Start date"
55
+ {...slotProps?.startDateFormControl}
56
+ className={classNames(styles.formControl, slotProps?.startDateFormControl?.className)}
57
+ >
58
+ <DsFormControl.DatePicker
59
+ value={startDate}
60
+ onChange={handleStartChange}
61
+ min={min}
62
+ max={effectiveStartMax}
63
+ withTime={withTime}
64
+ closeOnSelect={closeOnSelect}
65
+ disabled={disabled}
66
+ readOnly={readOnly}
67
+ disablePortal={disablePortal}
68
+ {...slotProps?.startDatePicker}
69
+ />
70
+ </DsFormControl>
71
+
72
+ <DsFormControl
73
+ label="End date"
74
+ {...slotProps?.endDateFormControl}
75
+ className={classNames(styles.formControl, slotProps?.startDateFormControl?.className)}
76
+ >
77
+ <DsFormControl.DatePicker
78
+ value={endDate}
79
+ onChange={handleEndChange}
80
+ min={effectiveEndMin}
81
+ max={max}
82
+ withTime={withTime}
83
+ closeOnSelect={closeOnSelect}
84
+ disabled={disabled}
85
+ readOnly={readOnly}
86
+ disablePortal={disablePortal}
87
+ {...slotProps?.endDatePicker}
88
+ />
89
+ </DsFormControl>
90
+
91
+ {showClearAll && (
92
+ <DsButton design="v1.2" buttonType="tertiary" className={styles.clearButton} onClick={handleClearAll}>
93
+ <DsIcon icon="close" size="small" />
94
+ {locale?.clearAllLabel ?? 'Clear all'}
95
+ </DsButton>
96
+ )}
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export default DsDateRangePicker;
@@ -0,0 +1,95 @@
1
+ import type { DsDatePickerProps } from '../ds-date-picker';
2
+ import type { DsFormControlProps } from '../ds-form-control';
3
+
4
+ export type DateRangeValue = [Date | null, Date | null];
5
+
6
+ export const dateRangePickerOrientations = ['horizontal', 'vertical'] as const;
7
+ export type DateRangePickerOrientation = (typeof dateRangePickerOrientations)[number];
8
+
9
+ export interface DsDateRangePickerLocale {
10
+ clearAllLabel?: string;
11
+ }
12
+
13
+ export type DateRangeFormControlSlotProps = Partial<Omit<DsFormControlProps, 'children'>>;
14
+
15
+ export interface DsDateRangePickerSlotProps {
16
+ startDatePicker?: DsDatePickerProps;
17
+ endDatePicker?: DsDatePickerProps;
18
+ startDateFormControl?: DateRangeFormControlSlotProps;
19
+ endDateFormControl?: DateRangeFormControlSlotProps;
20
+ }
21
+
22
+ export interface DsDateRangePickerProps {
23
+ /**
24
+ * Controlled value as a tuple of [startDate, endDate].
25
+ * `null` for either element means that date is empty.
26
+ * `undefined` means the component is uncontrolled.
27
+ */
28
+ value?: DateRangeValue;
29
+
30
+ /**
31
+ * Default value for uncontrolled mode
32
+ */
33
+ defaultValue?: DateRangeValue;
34
+
35
+ /**
36
+ * Callback when either date changes
37
+ */
38
+ onChange?: (value: DateRangeValue) => void;
39
+
40
+ /**
41
+ * Minimum allowed datetime for both pickers
42
+ */
43
+ min?: Date;
44
+
45
+ /**
46
+ * Maximum allowed datetime for both pickers
47
+ */
48
+ max?: Date;
49
+
50
+ /**
51
+ * Whether to show date pickers with time
52
+ * @default false
53
+ */
54
+ withTime?: boolean;
55
+
56
+ /**
57
+ * Layout orientation of the two pickers
58
+ * @default 'horizontal'
59
+ */
60
+ orientation?: DateRangePickerOrientation;
61
+
62
+ /**
63
+ * Whether to hide the "Clear all" button
64
+ * @default false
65
+ */
66
+ hideClearAll?: boolean;
67
+
68
+ /**
69
+ * Whether to close the calendar when a date is selected. This is ignored when withTime is true.
70
+ * @default true
71
+ */
72
+ closeOnSelect?: boolean;
73
+
74
+ disabled?: boolean;
75
+
76
+ readOnly?: boolean;
77
+
78
+ className?: string;
79
+
80
+ /**
81
+ * Whether to disable the portal for both calendar popups
82
+ * @default false
83
+ */
84
+ disablePortal?: boolean;
85
+
86
+ /**
87
+ * Localizable labels and text
88
+ */
89
+ locale?: DsDateRangePickerLocale;
90
+
91
+ /**
92
+ * Props forwarded to sub-component slots
93
+ */
94
+ slotProps?: DsDateRangePickerSlotProps;
95
+ }
@@ -0,0 +1,19 @@
1
+ export const earlierDate = (a: Date | undefined, b: Date | undefined): Date | undefined => {
2
+ if (!a) {
3
+ return b;
4
+ }
5
+ if (!b) {
6
+ return a;
7
+ }
8
+ return a < b ? a : b;
9
+ };
10
+
11
+ export const laterDate = (a: Date | undefined, b: Date | undefined): Date | undefined => {
12
+ if (!a) {
13
+ return b;
14
+ }
15
+ if (!b) {
16
+ return a;
17
+ }
18
+ return a > b ? a : b;
19
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { earlierDate, laterDate } from './ds-date-range-picker.utils';
3
+
4
+ const createDate = (year: number, month: number, day: number) => new Date(year, month - 1, day);
5
+
6
+ describe('Date Range Picker Utils', () => {
7
+ describe('earlierDate', () => {
8
+ it('should return the earlier of two dates', () => {
9
+ const jan = createDate(2024, 1, 1);
10
+ const mar = createDate(2024, 3, 15);
11
+
12
+ expect(earlierDate(jan, mar)).toBe(jan);
13
+ expect(earlierDate(mar, jan)).toBe(jan);
14
+ });
15
+
16
+ it('should return the other date when one is undefined', () => {
17
+ const date = createDate(2024, 6, 1);
18
+
19
+ expect(earlierDate(undefined, date)).toBe(date);
20
+ expect(earlierDate(date, undefined)).toBe(date);
21
+ });
22
+
23
+ it('should return undefined when both are undefined', () => {
24
+ expect(earlierDate(undefined, undefined)).toBeUndefined();
25
+ });
26
+ });
27
+
28
+ describe('laterDate', () => {
29
+ it('should return the later of two dates', () => {
30
+ const jan = createDate(2024, 1, 1);
31
+ const mar = createDate(2024, 3, 15);
32
+
33
+ expect(laterDate(jan, mar)).toBe(mar);
34
+ expect(laterDate(mar, jan)).toBe(mar);
35
+ });
36
+
37
+ it('should return the other date when one is undefined', () => {
38
+ const date = createDate(2024, 6, 1);
39
+
40
+ expect(laterDate(undefined, date)).toBe(date);
41
+ expect(laterDate(date, undefined)).toBe(date);
42
+ });
43
+
44
+ it('should return undefined when both are undefined', () => {
45
+ expect(laterDate(undefined, undefined)).toBeUndefined();
46
+ });
47
+ });
48
+ });
@@ -0,0 +1,9 @@
1
+ export { default as DsDateRangePicker } from './ds-date-range-picker';
2
+ export type {
3
+ DsDateRangePickerProps,
4
+ DsDateRangePickerLocale,
5
+ DsDateRangePickerSlotProps,
6
+ DateRangeFormControlSlotProps,
7
+ DateRangeValue,
8
+ DateRangePickerOrientation,
9
+ } from './ds-date-range-picker.types';
@@ -9,6 +9,7 @@ export interface UploadedFile extends File {
9
9
  progress: number;
10
10
  status: UploadFileStatus;
11
11
  errors?: FileError[];
12
+ originalFile: File;
12
13
  }
13
14
 
14
15
  export interface FileUploadOptions {
@@ -211,7 +211,7 @@ export function useFileUpload({
211
211
 
212
212
  try {
213
213
  const result = await adapter.upload({
214
- file,
214
+ file: file.originalFile,
215
215
  fileId,
216
216
  metadata,
217
217
  signal: abortController.signal,
@@ -1,7 +1,12 @@
1
1
  export { default as DsFileUpload } from './ds-file-upload';
2
2
  export { FileUpload } from './components/file-upload';
3
3
  export * from './ds-file-upload.types';
4
- export type { FileUploadAdapter, FileUploadOptions, FileUploadResult } from './ds-file-upload-api.types';
4
+ export type {
5
+ FileUploadAdapter,
6
+ FileUploadOptions,
7
+ FileUploadResult,
8
+ UploadedFile,
9
+ } from './ds-file-upload-api.types';
5
10
  export * from './hooks';
6
11
  export { FileUploadError, RetryableFileUploadError, FatalFileUploadError } from './errors/file-upload-errors';
7
12
  export type { AcceptedFileType, FileExtension } from './types/accept-types';
@@ -14,5 +14,6 @@ export function createUploadedFile(file: File, status: UploadFileStatus, errors?
14
14
  progress: 0,
15
15
  status,
16
16
  errors,
17
+ originalFile: file,
17
18
  } satisfies UploadedFile;
18
19
  }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createUploadedFile } from './file-factory';
3
+
4
+ describe('createUploadedFile', () => {
5
+ const makeFile = (name = 'test.txt', content = 'hello') =>
6
+ new File([content], name, { type: 'text/plain' });
7
+
8
+ it('should preserve originalFile reference', () => {
9
+ const mockFile = makeFile();
10
+ const uploaded = createUploadedFile(mockFile, 'pending');
11
+
12
+ expect(uploaded.originalFile).toBe(mockFile);
13
+ });
14
+
15
+ it('should preserve originalFile through object spread (retry scenario)', () => {
16
+ const mockFile = makeFile('data.bin', 'bytes');
17
+ const uploaded = createUploadedFile(mockFile, 'pending');
18
+ const data = { ...uploaded, status: 'uploading' as const };
19
+
20
+ expect(data.originalFile).toBe(mockFile);
21
+ });
22
+
23
+ it('should set upload metadata correctly', () => {
24
+ const name = 'test.txt';
25
+ const content = 'hello';
26
+ const mockFile = makeFile(name, content);
27
+ const uploaded = createUploadedFile(mockFile, 'error', ['TOO_LARGE']);
28
+
29
+ expect(uploaded.status).toBe('error');
30
+ expect(uploaded.progress).toBe(0);
31
+ expect(uploaded.errors).toEqual(['TOO_LARGE']);
32
+ expect(uploaded.name).toBe(name);
33
+ expect(uploaded.size).toBe(content.length);
34
+ expect(uploaded.type).toBe('text/plain');
35
+ expect(uploaded.id).toMatch(new RegExp(`^${name}-`));
36
+ });
37
+ });
@@ -10,6 +10,8 @@ import { DsPasswordInput } from '../ds-password-input';
10
10
  import type { DsFormControlDescriptionProps, DsFormControlProps } from './ds-form-control.types';
11
11
  import styles from './ds-form-control.module.scss';
12
12
  import { DsDateInput } from '../ds-date-input';
13
+ import { DsDatePicker } from '../ds-date-picker';
14
+ import { DsTimePicker } from '../ds-time-picker';
13
15
 
14
16
  const FormControlContext = createContext<{ controlId: string } | null>(null);
15
17
 
@@ -95,7 +97,10 @@ const DsFormControl = ({
95
97
  DsFormControl.TextInput = controlify(DsTextInput);
96
98
  DsFormControl.NumberInput = controlify(DsNumberInput);
97
99
  DsFormControl.PasswordInput = controlify(DsPasswordInput);
100
+ /** @deprecated DsDateInput is deprecated. Use DsDatePicker or DsDateRangePicker instead. */
98
101
  DsFormControl.DateInput = controlify(DsDateInput);
102
+ DsFormControl.DatePicker = controlify(DsDatePicker);
103
+ DsFormControl.TimePicker = controlify(DsTimePicker);
99
104
  DsFormControl.Textarea = controlify(DsTextarea);
100
105
  DsFormControl.Select = controlify(DsSelect);
101
106
  DsFormControl.Description = DsFormControlDescription;
@@ -0,0 +1,72 @@
1
+ .root {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ }
5
+
6
+ .root[data-orientation='vertical'] {
7
+ flex-direction: column;
8
+ }
9
+
10
+ .root[data-orientation='horizontal'] {
11
+ display: grid;
12
+ grid-template-columns: auto 1fr;
13
+ column-gap: var(--spacing-xs);
14
+ align-items: start;
15
+ }
16
+
17
+ .label {
18
+ padding-bottom: var(--spacing-4xs);
19
+ padding-left: var(--spacing-sm);
20
+ color: var(--color-font-main);
21
+ white-space: nowrap;
22
+ flex-shrink: 0;
23
+ }
24
+
25
+ .root[data-orientation='horizontal'] .label {
26
+ padding-bottom: 0;
27
+ padding-top: var(--spacing-3xs);
28
+ }
29
+
30
+ .valueContainer {
31
+ display: grid;
32
+ align-items: center;
33
+ width: 100%;
34
+ outline: none;
35
+ }
36
+
37
+ .root[data-orientation='horizontal'] .valueContainer {
38
+ justify-items: end;
39
+ min-width: 0;
40
+ }
41
+
42
+ .valueDisplay {
43
+ width: 100%;
44
+ min-width: 0;
45
+ min-height: 28px;
46
+ padding: var(--spacing-3xs) var(--spacing-sm);
47
+ color: var(--color-font-secondary);
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ overflow: hidden;
51
+ }
52
+
53
+ .root[data-orientation='horizontal'] .valueDisplay {
54
+ text-align: end;
55
+ }
56
+
57
+ .editorSlot {
58
+ display: none;
59
+ min-width: 0;
60
+ }
61
+
62
+ .valueContainer[data-editable]:hover .valueDisplay,
63
+ .valueContainer[data-editable]:focus-within .valueDisplay,
64
+ .valueContainer[data-editable]:has([data-state='open']) .valueDisplay {
65
+ display: none;
66
+ }
67
+
68
+ .valueContainer[data-editable]:hover .editorSlot,
69
+ .valueContainer[data-editable]:focus-within .editorSlot,
70
+ .valueContainer[data-editable]:has([data-state='open']) .editorSlot {
71
+ display: block;
72
+ }
@@ -0,0 +1,47 @@
1
+ import classNames from 'classnames';
2
+
3
+ import { DsTypography } from '../ds-typography';
4
+ import styles from './ds-key-value-pair.module.scss';
5
+ import type { DsKeyValuePairProps } from './ds-key-value-pair.types';
6
+
7
+ const DsKeyValuePair = ({
8
+ ref,
9
+ keyLabel,
10
+ value,
11
+ readOnly = false,
12
+ orientation = 'vertical',
13
+ editInput,
14
+ className,
15
+ style,
16
+ }: DsKeyValuePairProps) => {
17
+ const editable = !readOnly && !!editInput;
18
+
19
+ return (
20
+ <div
21
+ ref={ref}
22
+ className={classNames(styles.root, className)}
23
+ data-orientation={orientation}
24
+ data-readonly={readOnly || undefined}
25
+ style={style}
26
+ >
27
+ <DsTypography variant="body-sm-md" className={styles.label}>
28
+ {keyLabel}
29
+ </DsTypography>
30
+
31
+ <div
32
+ className={styles.valueContainer}
33
+ /* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- tabIndex enables keyboard focus which triggers :focus-within to reveal the editor */
34
+ tabIndex={editable ? 0 : undefined}
35
+ data-editable={editable || undefined}
36
+ >
37
+ <DsTypography variant="body-sm-reg" asChild>
38
+ <div className={styles.valueDisplay}>{value}</div>
39
+ </DsTypography>
40
+
41
+ {editable && <div className={styles.editorSlot}>{editInput}</div>}
42
+ </div>
43
+ </div>
44
+ );
45
+ };
46
+
47
+ export default DsKeyValuePair;
@@ -0,0 +1,24 @@
1
+ import type { CSSProperties, ReactNode, Ref } from 'react';
2
+
3
+ export const dsKeyValuePairOrientations = ['vertical', 'horizontal'] as const;
4
+ export type DsKeyValuePairOrientation = (typeof dsKeyValuePairOrientations)[number];
5
+
6
+ export interface DsKeyValuePairProps {
7
+ keyLabel: ReactNode;
8
+
9
+ /** Shown when the editor is not active (read-only or not hovered). */
10
+ value?: ReactNode;
11
+
12
+ /** @default false */
13
+ readOnly?: boolean;
14
+
15
+ /** @default 'vertical' */
16
+ orientation?: DsKeyValuePairOrientation;
17
+
18
+ /** Editor content revealed on hover/focus when not read-only. */
19
+ editInput?: ReactNode;
20
+
21
+ ref?: Ref<HTMLDivElement>;
22
+ className?: string;
23
+ style?: CSSProperties;
24
+ }
@@ -0,0 +1,3 @@
1
+ export { default as DsKeyValuePair } from './ds-key-value-pair';
2
+ export type { DsKeyValuePairProps, DsKeyValuePairOrientation } from './ds-key-value-pair.types';
3
+ export { dsKeyValuePairOrientations } from './ds-key-value-pair.types';
@@ -9,7 +9,7 @@ import SpinnerIcon from './spinner-icon';
9
9
  */
10
10
  const DsLoader = ({ variant = 'spinner', className, ...props }: DsLoaderProps) => {
11
11
  return (
12
- <div className={classNames(styles.loaderContainer, className)} {...props}>
12
+ <div className={classNames(styles.loaderContainer, className)} role="progressbar" {...props}>
13
13
  {variant === 'pulsing' ? <PulsingIcon /> : <SpinnerIcon />}
14
14
  </div>
15
15
  );
@@ -74,6 +74,6 @@
74
74
  @include status-ghost(var(--color-background-danger-strong-hover), var(--color-status-bg-failed));
75
75
  }
76
76
 
77
- .label {
77
+ .label::first-letter {
78
78
  text-transform: capitalize;
79
79
  }
@@ -4,7 +4,7 @@ import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
4
4
  import styles from './ds-table-body-virtualized.module.scss';
5
5
  import { DsTableRowVirtualized } from '../ds-table-row-virtualized';
6
6
  import type { DsTableBodyVirtualizedProps } from './ds-table-body-virtualized.types';
7
- import { TableBody } from '../core-table';
7
+ import { TableBody, TableRow, TableCell } from '../core-table';
8
8
  import { EMPTY_TABLE_STATE_TEXT } from '../../utils/constants';
9
9
 
10
10
  export const DsTableBodyVirtualized = <TData,>({
@@ -138,7 +138,9 @@ function DsTableBody<TData>({
138
138
  );
139
139
  })
140
140
  ) : (
141
- <div className={styles.emptyState}>{emptyState || EMPTY_TABLE_STATE_TEXT}</div>
141
+ <TableRow>
142
+ <TableCell className={styles.emptyState}>{emptyState || EMPTY_TABLE_STATE_TEXT}</TableCell>
143
+ </TableRow>
142
144
  )}
143
145
  </TableBody>
144
146
  );
@@ -0,0 +1 @@
1
+ export * from './time-item';
@@ -0,0 +1,37 @@
1
+ $time-item-height: 32px;
2
+ $time-item-width: 48px;
3
+
4
+ .container {
5
+ display: flex;
6
+ width: $time-item-width;
7
+ height: $time-item-height;
8
+ flex-shrink: 0;
9
+ flex-direction: column;
10
+ justify-content: center;
11
+ align-items: center;
12
+ border-radius: 8px;
13
+ background: transparent;
14
+ color: var(--color-font-main);
15
+
16
+ &.selected {
17
+ background: var(--color-background-selected);
18
+ color: var(--color-font-on-action);
19
+ }
20
+
21
+ &.disabled {
22
+ color: var(--color-font-disabled);
23
+ pointer-events: none;
24
+ }
25
+
26
+ &:not(.selected, .disabled) {
27
+ &:hover {
28
+ border: 1px solid var(--color-border);
29
+ background: var(--color-background-action-hover-weak);
30
+ }
31
+
32
+ &:focus-visible {
33
+ border: 2px solid var(--color-border-action-secondary);
34
+ background: var(--color-background-action-hover-weak);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,26 @@
1
+ import classNames from 'classnames';
2
+ import type { TimeItemProps } from './time-item.types';
3
+ import styles from './time-item.module.scss';
4
+ import { DsTypography } from '../../../ds-typography';
5
+
6
+ export const TimeItem = ({ ref, className, selected, disabled, onClick, label }: TimeItemProps) => {
7
+ return (
8
+ <button
9
+ ref={ref}
10
+ type="button"
11
+ role="option"
12
+ aria-selected={selected}
13
+ aria-disabled={disabled || undefined}
14
+ disabled={disabled}
15
+ className={classNames(
16
+ styles.container,
17
+ selected && styles.selected,
18
+ disabled && styles.disabled,
19
+ className,
20
+ )}
21
+ onClick={onClick}
22
+ >
23
+ <DsTypography variant="body-md-md">{label}</DsTypography>
24
+ </button>
25
+ );
26
+ };
@@ -0,0 +1,10 @@
1
+ import type { RefObject } from 'react';
2
+
3
+ export interface TimeItemProps {
4
+ ref?: RefObject<HTMLButtonElement | null>;
5
+ className?: string;
6
+ label: string;
7
+ selected: boolean;
8
+ disabled?: boolean;
9
+ onClick: () => void;
10
+ }
@@ -0,0 +1,2 @@
1
+ export type { TimePeriod, TimeScrollerProps } from './time-scroller.types';
2
+ export * from './time-scroller';
@@ -0,0 +1,25 @@
1
+ $column-width: 56px;
2
+ $column-max-height: 280px;
3
+
4
+ .container {
5
+ display: flex;
6
+ padding: var(--spacing-xs);
7
+ background: var(--color-background);
8
+ box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.22);
9
+ }
10
+
11
+ .column {
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ width: $column-width;
16
+ gap: var(--spacing-xs);
17
+ max-height: $column-max-height;
18
+ overflow-y: auto;
19
+ scroll-snap-type: y mandatory;
20
+
21
+ scrollbar-width: none;
22
+ &::-webkit-scrollbar {
23
+ display: none;
24
+ }
25
+ }