@drivenets/design-system 0.6.1 → 0.7.1

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 (138) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/index.cjs +12297 -8255
  3. package/dist/index.d.cts +817 -118
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +817 -118
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +12280 -8259
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +25 -23
  11. package/src/components/ds-autocomplete/ds-autocomplete.module.scss +2 -1
  12. package/src/components/ds-autocomplete/ds-autocomplete.tsx +47 -40
  13. package/src/components/ds-autocomplete/ds-autocomplete.types.tsx +37 -14
  14. package/src/components/ds-button/versions/ds-button-new/ds-button-new.module.scss +3 -7
  15. package/src/components/ds-comment-bubble/components/ds-comment-thread/ds-comment-thread.module.scss +62 -0
  16. package/src/components/ds-comment-bubble/components/ds-comment-thread/ds-comment-thread.tsx +83 -0
  17. package/src/components/ds-comment-bubble/components/ds-comment-thread/ds-comment-thread.types.ts +12 -0
  18. package/src/components/ds-comment-bubble/components/ds-comment-thread/index.ts +2 -0
  19. package/src/components/ds-comment-bubble/components/ds-thread-item/ds-thread-item.module.scss +149 -0
  20. package/src/components/ds-comment-bubble/components/ds-thread-item/ds-thread-item.tsx +147 -0
  21. package/src/components/ds-comment-bubble/components/ds-thread-item/ds-thread-item.types.ts +15 -0
  22. package/src/components/ds-comment-bubble/components/ds-thread-item/index.ts +2 -0
  23. package/src/components/ds-comment-bubble/ds-comment-bubble.module.scss +298 -0
  24. package/src/components/ds-comment-bubble/ds-comment-bubble.tsx +241 -0
  25. package/src/components/ds-comment-bubble/ds-comment-bubble.types.ts +27 -0
  26. package/src/components/ds-comment-bubble/hooks/index.ts +3 -0
  27. package/src/components/ds-comment-bubble/hooks/use-auto-resize.ts +17 -0
  28. package/src/components/ds-comment-bubble/hooks/use-click-outside.ts +22 -0
  29. package/src/components/ds-comment-bubble/hooks/use-scroll-overflow.ts +27 -0
  30. package/src/components/ds-comment-bubble/index.ts +6 -0
  31. package/src/components/ds-comment-card/ds-comment-card.module.scss +162 -0
  32. package/src/components/ds-comment-card/ds-comment-card.tsx +171 -0
  33. package/src/components/ds-comment-card/ds-comment-card.types.ts +45 -0
  34. package/src/components/ds-comment-card/index.ts +2 -0
  35. package/src/components/ds-comment-indicator/ds-comment-indicator.module.scss +65 -0
  36. package/src/components/ds-comment-indicator/ds-comment-indicator.tsx +48 -0
  37. package/src/components/ds-comment-indicator/ds-comment-indicator.types.ts +13 -0
  38. package/src/components/ds-comment-indicator/index.ts +2 -0
  39. package/src/components/ds-comments-drawer/comments-filters.test.ts +483 -0
  40. package/src/components/ds-comments-drawer/comments-filters.ts +134 -0
  41. package/src/components/ds-comments-drawer/comments-filters.types.ts +14 -0
  42. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.module.scss +92 -0
  43. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.tsx +219 -0
  44. package/src/components/ds-comments-drawer/components/comments-filter-modal/comments-filter-modal.types.ts +12 -0
  45. package/src/components/ds-comments-drawer/components/comments-filter-modal/index.ts +2 -0
  46. package/src/components/ds-comments-drawer/ds-comments-drawer.module.scss +67 -0
  47. package/src/components/ds-comments-drawer/ds-comments-drawer.tsx +215 -0
  48. package/src/components/ds-comments-drawer/ds-comments-drawer.types.ts +23 -0
  49. package/src/components/ds-comments-drawer/index.ts +3 -0
  50. package/src/components/ds-date-input/ds-date-input.tsx +3 -1
  51. package/src/components/ds-date-input/ds-date-input.types.tsx +6 -0
  52. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  53. package/src/components/ds-expandable-text-input/ds-expandable-text-input.tsx +8 -4
  54. package/src/components/ds-file-upload/components/file-upload/file-upload.tsx +2 -1
  55. package/src/components/ds-file-upload/components/file-upload/file-upload.types.tsx +5 -0
  56. package/src/components/ds-file-upload/types/accept-types.ts +0 -1
  57. package/src/components/ds-filter-status-icon/ds-filter-status-icon.module.scss +19 -0
  58. package/src/components/ds-filter-status-icon/ds-filter-status-icon.tsx +35 -0
  59. package/src/components/ds-filter-status-icon/ds-filter-status-icon.types.ts +31 -0
  60. package/src/components/ds-filter-status-icon/index.ts +3 -0
  61. package/src/components/ds-icon/custom-icons/index.ts +10 -0
  62. package/src/components/ds-icon/custom-icons/special-failed.tsx +16 -0
  63. package/src/components/ds-icon/custom-icons/special-paused.tsx +16 -0
  64. package/src/components/ds-icon/custom-icons/special-running.tsx +16 -0
  65. package/src/components/ds-icon/custom-icons/special-warning.tsx +16 -0
  66. package/src/components/ds-panel/ds-panel.module.scss +3 -3
  67. package/src/components/ds-progress-arc/ds-progress-arc.module.scss +57 -0
  68. package/src/components/ds-progress-arc/ds-progress-arc.tsx +111 -0
  69. package/src/components/ds-progress-arc/ds-progress-arc.types.ts +34 -0
  70. package/src/components/ds-progress-arc/index.ts +3 -0
  71. package/src/components/ds-progress-arc/utils.test.ts +79 -0
  72. package/src/components/ds-progress-arc/utils.ts +61 -0
  73. package/src/components/ds-progress-donut/ds-progress-donut.module.scss +67 -0
  74. package/src/components/ds-progress-donut/ds-progress-donut.tsx +83 -0
  75. package/src/components/ds-progress-donut/ds-progress-donut.types.ts +38 -0
  76. package/src/components/ds-progress-donut/index.ts +2 -0
  77. package/src/components/ds-progress-donut/utils.ts +1 -0
  78. package/src/components/ds-progress-linear/ds-progress-linear.module.scss +100 -0
  79. package/src/components/ds-progress-linear/ds-progress-linear.tsx +107 -0
  80. package/src/components/ds-progress-linear/ds-progress-linear.types.ts +72 -0
  81. package/src/components/ds-progress-linear/index.ts +2 -0
  82. package/src/components/ds-progress-linear/utils.test.ts +70 -0
  83. package/src/components/ds-progress-linear/utils.ts +7 -0
  84. package/src/components/ds-radio-group/ds-radio-group.tsx +2 -0
  85. package/src/components/ds-select/ds-select.tsx +2 -1
  86. package/src/components/ds-select/ds-select.types.tsx +7 -0
  87. package/src/components/ds-stepper/components/ds-step.tsx +1 -1
  88. package/src/components/ds-stepper/ds-stepper.module.scss +16 -2
  89. package/src/components/ds-stepper/ds-stepper.tsx +3 -1
  90. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +9 -0
  91. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +137 -0
  92. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +22 -0
  93. package/src/components/ds-table/components/ds-table-body-virtualized/index.ts +2 -0
  94. package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +3 -1
  95. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +7 -10
  96. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +3 -4
  97. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +13 -53
  98. package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +32 -85
  99. package/src/components/ds-table/components/ds-table-row/ds-table-row.types.ts +0 -6
  100. package/src/components/ds-table/components/ds-table-row-expandable-cell/ds-table-row-expandable-cell.module.scss +10 -0
  101. package/src/components/ds-table/components/ds-table-row-expandable-cell/ds-table-row-expandable-cell.tsx +45 -0
  102. package/src/components/ds-table/components/ds-table-row-expandable-cell/ds-table-row-expandable-cell.types.ts +7 -0
  103. package/src/components/ds-table/components/ds-table-row-expandable-cell/index.ts +2 -0
  104. package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.module.scss +6 -0
  105. package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.tsx +30 -0
  106. package/src/components/ds-table/components/ds-table-row-selectable-cell/ds-table-row-selectable-cell.types.ts +6 -0
  107. package/src/components/ds-table/components/ds-table-row-selectable-cell/index.ts +2 -0
  108. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +80 -0
  109. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +95 -0
  110. package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +11 -0
  111. package/src/components/ds-table/components/ds-table-row-virtualized/index.ts +2 -0
  112. package/src/components/ds-table/context/ds-table-context.tsx +0 -9
  113. package/src/components/ds-table/ds-table.module.scss +4 -17
  114. package/src/components/ds-table/ds-table.tsx +44 -77
  115. package/src/components/ds-table/ds-table.types.ts +57 -12
  116. package/src/components/ds-table/index.ts +1 -0
  117. package/src/components/ds-table/styles/_mixins.scss +15 -0
  118. package/src/components/ds-table/styles/_variables.scss +2 -2
  119. package/src/components/ds-table/utils/{column-styling.ts → column-size.ts} +4 -14
  120. package/src/components/ds-table/utils/constants.ts +4 -0
  121. package/src/components/ds-tag/index.tsx +1 -0
  122. package/src/components/ds-tag-filter/ds-tag-filter.module.scss +13 -15
  123. package/src/components/ds-tag-filter/ds-tag-filter.tsx +38 -72
  124. package/src/components/ds-tag-filter/ds-tag-filter.types.ts +2 -3
  125. package/src/components/ds-tag-filter/hooks/use-tag-overflow-calculation.ts +12 -62
  126. package/src/components/ds-tag-filter/utils.test.ts +2 -108
  127. package/src/components/ds-tag-filter/utils.ts +2 -12
  128. package/src/components/ds-tree/ds-tree.module.scss +283 -0
  129. package/src/components/ds-tree/ds-tree.tsx +172 -0
  130. package/src/components/ds-tree/ds-tree.types.ts +83 -0
  131. package/src/components/ds-tree/ds-tree.utils.ts +11 -0
  132. package/src/components/ds-tree/index.ts +3 -0
  133. package/src/index.ts +9 -0
  134. package/src/utils/format-relative-time.test.ts +74 -0
  135. package/src/utils/format-relative-time.ts +52 -0
  136. package/src/utils/numbers.test.ts +24 -0
  137. package/src/utils/numbers.ts +1 -0
  138. package/src/components/ds-table/utils/create-columns-grid-template.ts +0 -23
@@ -0,0 +1,147 @@
1
+ import { useState, useEffect } from 'react';
2
+ import classNames from 'classnames';
3
+ import styles from './ds-thread-item.module.scss';
4
+ import { DsAvatar } from '../../../ds-avatar';
5
+ import { DsButton } from '../../../ds-button';
6
+ import { DsIcon } from '../../../ds-icon';
7
+ import { DsDropdownMenu } from '../../../ds-dropdown-menu';
8
+ import { DsTextarea } from '../../../ds-textarea';
9
+ import { formatRelativeTime } from '../../../../utils/format-relative-time';
10
+ import type { DsThreadItemProps } from './ds-thread-item.types';
11
+
12
+ export const DsThreadItem = ({
13
+ id,
14
+ author,
15
+ content,
16
+ createdAt,
17
+ isCommentAuthorMessage = false,
18
+ canModify = false,
19
+ onEdit,
20
+ onDelete,
21
+ onMarkUnread,
22
+ onResolved,
23
+ className,
24
+ }: DsThreadItemProps) => {
25
+ const [isEditing, setIsEditing] = useState(false);
26
+ const [editContent, setEditContent] = useState(content);
27
+
28
+ useEffect(() => {
29
+ if (!isEditing) {
30
+ setEditContent(content);
31
+ }
32
+ }, [content, isEditing]);
33
+
34
+ const isSaveDisabled = !editContent.trim() || editContent === content;
35
+
36
+ const handleSaveEdit = () => {
37
+ if (isSaveDisabled) {
38
+ return;
39
+ }
40
+
41
+ onEdit?.(id, editContent);
42
+ setIsEditing(false);
43
+ };
44
+
45
+ const handleCancelEdit = () => {
46
+ setEditContent(content);
47
+ setIsEditing(false);
48
+ };
49
+
50
+ const hasEditActions = canModify && (onEdit || onDelete);
51
+ const showActions = !isEditing && (hasEditActions || onMarkUnread || onResolved);
52
+
53
+ return (
54
+ <div
55
+ className={classNames(
56
+ styles.threadItem,
57
+ {
58
+ [styles.alignRight]: !isCommentAuthorMessage,
59
+ },
60
+ className,
61
+ )}
62
+ >
63
+ <div className={styles.avatarWrapper}>
64
+ <DsAvatar name={author.name} src={author.avatarSrc} size="sm" />
65
+ </div>
66
+
67
+ <div className={styles.content}>
68
+ <div className={styles.header}>
69
+ <span className={styles.authorName}>{author.name}</span>
70
+ <span className={styles.timestamp}>{formatRelativeTime(createdAt)}</span>
71
+ </div>
72
+
73
+ {isEditing ? (
74
+ <div className={styles.editContainer}>
75
+ <DsTextarea
76
+ className={styles.editTextarea}
77
+ value={editContent}
78
+ onValueChange={setEditContent}
79
+ rows={3}
80
+ />
81
+ <div className={styles.editActions}>
82
+ <DsButton design="v1.2" buttonType="tertiary" size="small" onClick={handleCancelEdit}>
83
+ Cancel
84
+ </DsButton>
85
+ <DsButton design="v1.2" size="small" onClick={handleSaveEdit} disabled={isSaveDisabled}>
86
+ Save
87
+ </DsButton>
88
+ </div>
89
+ </div>
90
+ ) : (
91
+ <p className={styles.message}>{content}</p>
92
+ )}
93
+ </div>
94
+
95
+ {showActions && (
96
+ <div className={styles.actions}>
97
+ {hasEditActions && (
98
+ <DsDropdownMenu.Root>
99
+ <DsDropdownMenu.Trigger asChild>
100
+ <DsButton
101
+ design="v1.2"
102
+ buttonType="tertiary"
103
+ size="small"
104
+ aria-label="More actions"
105
+ className={styles.actionButton}
106
+ contentClassName={styles.iconButtonContent}
107
+ >
108
+ <DsIcon icon="more_vert" size="tiny" />
109
+ </DsButton>
110
+ </DsDropdownMenu.Trigger>
111
+ <DsDropdownMenu.Content className={styles.dropdownContent}>
112
+ {onEdit && (
113
+ <DsDropdownMenu.Item value="edit" onClick={() => setIsEditing(true)}>
114
+ Edit
115
+ </DsDropdownMenu.Item>
116
+ )}
117
+ {onMarkUnread && (
118
+ <DsDropdownMenu.Item value="mark-unread" onClick={() => onMarkUnread(id)}>
119
+ Mark as &apos;Unread&apos;
120
+ </DsDropdownMenu.Item>
121
+ )}
122
+ {onDelete && (
123
+ <DsDropdownMenu.Item value="delete" onClick={() => onDelete(id)}>
124
+ Delete
125
+ </DsDropdownMenu.Item>
126
+ )}
127
+ </DsDropdownMenu.Content>
128
+ </DsDropdownMenu.Root>
129
+ )}
130
+ {onResolved && (
131
+ <DsButton
132
+ design="v1.2"
133
+ buttonType="tertiary"
134
+ size="small"
135
+ onClick={() => onResolved(id)}
136
+ aria-label="Mark message as resolved"
137
+ className={styles.actionButton}
138
+ contentClassName={styles.iconButtonContent}
139
+ >
140
+ <DsIcon icon="check_circle" size="tiny" />
141
+ </DsButton>
142
+ )}
143
+ </div>
144
+ )}
145
+ </div>
146
+ );
147
+ };
@@ -0,0 +1,15 @@
1
+ import type { CommentAuthor } from '../../../ds-comment-card';
2
+
3
+ export interface DsThreadItemProps {
4
+ id: string;
5
+ author: CommentAuthor;
6
+ content: string;
7
+ createdAt: Date;
8
+ isCommentAuthorMessage?: boolean;
9
+ canModify?: boolean;
10
+ onEdit?: (messageId: string, newContent: string) => void;
11
+ onDelete?: (messageId: string) => void;
12
+ onMarkUnread?: (messageId: string) => void;
13
+ onResolved?: (messageId: string) => void;
14
+ className?: string;
15
+ }
@@ -0,0 +1,2 @@
1
+ export { DsThreadItem } from './ds-thread-item';
2
+ export * from './ds-thread-item.types';
@@ -0,0 +1,298 @@
1
+ $bubble-width-initial: 240px;
2
+ $bubble-width-typing: 320px;
3
+ $bubble-width-thread: 420px;
4
+ $bubble-height-thread: 644px;
5
+ $bubble-border-radius-pill: 16px;
6
+ $bubble-border-radius-standard: 4px;
7
+ $input-min-height-initial: 20px;
8
+ $input-min-height-typing: 40px;
9
+ $input-min-height-thread: 28px;
10
+ $input-max-height: 480px;
11
+ $icon-button-size: 16px;
12
+ $scrollbar-width-textarea: 4px;
13
+ $scrollbar-border-radius-pill: 100px;
14
+ $dropdown-min-width: 160px;
15
+ $border-width: 1px;
16
+ $checkbox-border-radius: 2px;
17
+
18
+ $transition-duration: 0.2s;
19
+
20
+ @mixin icon-button-size {
21
+ &#{&} {
22
+ width: $icon-button-size;
23
+ height: $icon-button-size;
24
+ min-width: $icon-button-size;
25
+ min-height: $icon-button-size;
26
+ padding: 0;
27
+ border: none;
28
+ }
29
+ }
30
+
31
+ .iconButtonContent {
32
+ height: $icon-button-size !important;
33
+ min-height: $icon-button-size !important;
34
+ padding: 0 !important;
35
+ border: none !important;
36
+ }
37
+
38
+ .bubble {
39
+ display: flex;
40
+ flex-direction: column;
41
+ width: $bubble-width-thread;
42
+ background: var(--color-background);
43
+ border: $border-width solid var(--color-primary-700-default);
44
+ border-radius: $bubble-border-radius-pill;
45
+ box-shadow: 0 6px 13px 0 rgba(4, 69, 204, 0.2);
46
+ overflow: hidden;
47
+ transition:
48
+ width $transition-duration ease,
49
+ border-radius $transition-duration ease,
50
+ border-color $transition-duration ease,
51
+ padding $transition-duration ease;
52
+
53
+ &.actionRequired {
54
+ border-color: var(--color-background-warning-strong);
55
+ }
56
+ }
57
+
58
+ .initial {
59
+ width: $bubble-width-initial;
60
+ border-width: $border-width;
61
+ padding: var(--spacing-sm) var(--spacing-standard);
62
+ border-radius: $bubble-border-radius-pill;
63
+
64
+ .inputArea {
65
+ padding: 0;
66
+ gap: var(--spacing-xs);
67
+ align-items: center;
68
+ }
69
+
70
+ .inputWrapper {
71
+ background: transparent;
72
+ padding: 0;
73
+ min-height: auto;
74
+ flex: 1;
75
+ }
76
+
77
+ .textarea {
78
+ padding: 0;
79
+ min-height: $input-min-height-initial;
80
+ line-height: var(--line-height-sm);
81
+ overflow: hidden;
82
+ resize: none;
83
+
84
+ &::placeholder {
85
+ color: var(--color-font-placeholder);
86
+ }
87
+ }
88
+ }
89
+
90
+ .typing {
91
+ width: $bubble-width-typing;
92
+ border-width: $border-width;
93
+ padding: var(--spacing-sm) var(--spacing-standard) 0;
94
+ border-radius: $bubble-border-radius-pill;
95
+
96
+ .inputArea {
97
+ padding: 0 0 var(--spacing-sm);
98
+ border-bottom: $border-width solid var(--color-border-secondary);
99
+ }
100
+
101
+ .inputWrapper {
102
+ min-height: $input-min-height-typing;
103
+ padding: 0;
104
+ background: transparent;
105
+ }
106
+ }
107
+
108
+ .thread {
109
+ height: $bubble-height-thread;
110
+ padding: 0;
111
+ border-radius: $bubble-border-radius-standard;
112
+ display: flex;
113
+ flex-direction: column;
114
+
115
+ .inputArea {
116
+ border-top: $border-width solid var(--color-border-secondary);
117
+ padding: var(--spacing-xs) var(--spacing-standard);
118
+ flex-shrink: 0;
119
+ }
120
+
121
+ .inputWrapper {
122
+ background: var(--color-background);
123
+ padding: var(--spacing-xs) var(--spacing-sm);
124
+ border-radius: $bubble-border-radius-standard;
125
+ min-height: $input-min-height-thread;
126
+ }
127
+ }
128
+
129
+ .header {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ gap: var(--spacing-xs);
134
+ padding: var(--spacing-xs) var(--spacing-standard) var(--spacing-xs);
135
+ border-bottom: $border-width solid var(--color-border);
136
+ flex-shrink: 0;
137
+ }
138
+
139
+ .headerLeft {
140
+ display: flex;
141
+ align-items: center;
142
+ flex: 1;
143
+ min-width: 0;
144
+ }
145
+
146
+ .headerInfo {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: var(--spacing-xs);
150
+ }
151
+
152
+ .commentId {
153
+ font-size: var(--font-size-sm);
154
+ line-height: var(--line-height-sm);
155
+ color: var(--color-font-secondary);
156
+ }
157
+
158
+ .actionRequiredBadge {
159
+ font-size: var(--font-size-sm);
160
+ line-height: var(--line-height-sm);
161
+ color: var(--color-font-warning);
162
+ font-weight: var(--font-weight-regular);
163
+ }
164
+
165
+ .headerRight {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: var(--spacing-xs);
169
+ }
170
+
171
+ .headerActions {
172
+ display: flex;
173
+ align-items: center;
174
+ gap: var(--spacing-xs);
175
+ opacity: 0;
176
+ transition: opacity $transition-duration ease;
177
+
178
+ .bubble:hover & {
179
+ opacity: 1;
180
+ }
181
+ }
182
+
183
+ .headerActionButton {
184
+ @include icon-button-size;
185
+ opacity: 1;
186
+ }
187
+
188
+ .inputArea {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: var(--spacing-xs);
192
+ }
193
+
194
+ .inputWrapper {
195
+ display: flex;
196
+ align-items: center;
197
+ flex: 1;
198
+ gap: var(--spacing-xs);
199
+ min-height: $input-min-height-thread;
200
+ }
201
+
202
+ .searchIcon {
203
+ color: var(--color-font-main);
204
+ flex-shrink: 0;
205
+ }
206
+
207
+ .infoIcon {
208
+ color: var(--color-font-main);
209
+ flex-shrink: 0;
210
+ }
211
+
212
+ .textarea {
213
+ flex: 1;
214
+ min-height: $input-min-height-initial;
215
+ max-height: $input-max-height;
216
+ padding: 0;
217
+ border: none;
218
+ background: transparent;
219
+ font-size: var(--font-size-sm);
220
+ line-height: var(--line-height-sm);
221
+ font-family: inherit;
222
+ color: var(--color-font-main);
223
+ resize: none;
224
+ overflow-y: auto;
225
+
226
+ &::placeholder {
227
+ color: var(--color-font-placeholder);
228
+ }
229
+
230
+ &:focus {
231
+ outline: none;
232
+ }
233
+
234
+ &::-webkit-scrollbar {
235
+ width: $scrollbar-width-textarea;
236
+ }
237
+
238
+ &::-webkit-scrollbar-track {
239
+ background: transparent;
240
+ }
241
+
242
+ &::-webkit-scrollbar-thumb {
243
+ background: var(--color-background-tertiary);
244
+ border-radius: $scrollbar-border-radius-pill;
245
+ }
246
+ }
247
+
248
+ .footer {
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: space-between;
252
+ padding: var(--spacing-xs) 0;
253
+ }
254
+
255
+ .actionRequiredCheckbox {
256
+ &.checked {
257
+ [data-state='checked'] {
258
+ background: var(--color-background-warning-strong);
259
+ border-color: var(--color-border-warning);
260
+ border-radius: $checkbox-border-radius;
261
+ }
262
+
263
+ label {
264
+ color: var(--color-font-warning);
265
+ }
266
+ }
267
+ }
268
+
269
+ .sendButton {
270
+ flex-shrink: 0;
271
+ }
272
+
273
+ .dropdownContent {
274
+ min-width: $dropdown-min-width;
275
+
276
+ &#{&} {
277
+ z-index: 1300;
278
+ }
279
+ }
280
+
281
+ .dropdownItem {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: var(--spacing-xs);
285
+ padding: var(--spacing-xs) var(--spacing-sm);
286
+ font-size: var(--font-size-sm);
287
+ line-height: var(--line-height-sm);
288
+ color: var(--color-font-main);
289
+ cursor: pointer;
290
+
291
+ &:hover {
292
+ background: var(--color-background-secondary);
293
+ }
294
+
295
+ &.danger {
296
+ color: var(--color-font-danger);
297
+ }
298
+ }
@@ -0,0 +1,241 @@
1
+ import { useRef, useCallback } from 'react';
2
+ import classNames from 'classnames';
3
+ import styles from './ds-comment-bubble.module.scss';
4
+ import type { DsCommentBubbleProps } from './ds-comment-bubble.types';
5
+ import { DsCommentThread } from './components/ds-comment-thread';
6
+ import { DsButton } from '../ds-button';
7
+ import { DsCheckbox } from '../ds-checkbox';
8
+ import { DsIcon } from '../ds-icon';
9
+ import { DsTag } from '../ds-tag';
10
+ import { DsDropdownMenu } from '../ds-dropdown-menu';
11
+ import { useClickOutside, useAutoResize } from './hooks';
12
+ import { mergeRefs } from '../ds-table/utils/merge-refs';
13
+
14
+ export const DsCommentBubble = ({
15
+ ref,
16
+ comment,
17
+ currentUser,
18
+ actionRequired = false,
19
+ onActionRequiredChange,
20
+ value = '',
21
+ onValueChange,
22
+ onSend,
23
+ onClose,
24
+ onResolve,
25
+ onToggleActionRequired,
26
+ onForward,
27
+ onMarkUnread,
28
+ onCopyLink,
29
+ onDelete,
30
+ onEditMessage,
31
+ onDeleteMessage,
32
+ onMessageMarkUnread,
33
+ onMessageResolved,
34
+ referenceTag,
35
+ className,
36
+ style,
37
+ }: DsCommentBubbleProps) => {
38
+ const hasThread = Boolean(comment?.messages.length);
39
+ const hasContent = value.trim().length > 0;
40
+ const isTypingMode = !hasThread && hasContent;
41
+ const isInitialMode = !hasThread && !hasContent;
42
+ const placeholder = hasThread ? 'Reply' : 'Add a comment';
43
+ const showFooter = isTypingMode;
44
+ const showHeader = hasThread && Boolean(comment);
45
+ const maxTextareaHeight = 480;
46
+
47
+ const bubbleRef = useRef<HTMLDivElement>(null);
48
+ const mergedRef = mergeRefs(ref, bubbleRef);
49
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
50
+
51
+ const handleClickOutside = useCallback(() => {
52
+ if (isInitialMode) {
53
+ onClose?.();
54
+ }
55
+ }, [isInitialMode, onClose]);
56
+
57
+ useClickOutside(bubbleRef, handleClickOutside, isInitialMode);
58
+ useAutoResize(textareaRef, value, maxTextareaHeight);
59
+
60
+ const handleSend = () => {
61
+ if (hasContent && onSend) {
62
+ onSend(value, actionRequired);
63
+ }
64
+ };
65
+
66
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
67
+ if (e.key === 'Enter' && !e.shiftKey) {
68
+ e.preventDefault();
69
+ handleSend();
70
+ }
71
+ };
72
+
73
+ const renderReferenceTag = () => {
74
+ if (!referenceTag) {
75
+ return null;
76
+ }
77
+
78
+ if (typeof referenceTag === 'string') {
79
+ return (
80
+ <DsTag
81
+ size="small"
82
+ label={referenceTag}
83
+ slots={{
84
+ icon: <DsIcon icon="sell" size="tiny" />,
85
+ }}
86
+ />
87
+ );
88
+ }
89
+
90
+ return referenceTag;
91
+ };
92
+
93
+ return (
94
+ <div
95
+ ref={mergedRef}
96
+ className={classNames(
97
+ styles.bubble,
98
+ {
99
+ [styles.actionRequired]: actionRequired,
100
+ [styles.initial]: isInitialMode,
101
+ [styles.typing]: isTypingMode,
102
+ [styles.thread]: hasThread,
103
+ },
104
+ className,
105
+ )}
106
+ style={style}
107
+ role="dialog"
108
+ aria-label={
109
+ isInitialMode
110
+ ? 'Add new comment'
111
+ : `Comment thread${comment?.numericId ? ` #${String(comment.numericId)}` : ''}`
112
+ }
113
+ >
114
+ {showHeader && comment && (
115
+ <div className={styles.header}>
116
+ <div className={styles.headerLeft}>
117
+ <div className={styles.headerInfo}>
118
+ <span className={styles.commentId}>#{comment.numericId}</span>
119
+ {renderReferenceTag()}
120
+ {actionRequired && <span className={styles.actionRequiredBadge}>Action required</span>}
121
+ </div>
122
+ </div>
123
+
124
+ <div className={styles.headerRight}>
125
+ <div className={styles.headerActions}>
126
+ <DsDropdownMenu.Root>
127
+ <DsDropdownMenu.Trigger asChild>
128
+ <DsButton
129
+ design="v1.2"
130
+ buttonType="tertiary"
131
+ size="small"
132
+ aria-label="More actions"
133
+ className={styles.headerActionButton}
134
+ contentClassName={styles.iconButtonContent}
135
+ >
136
+ <DsIcon icon="more_vert" size="tiny" />
137
+ </DsButton>
138
+ </DsDropdownMenu.Trigger>
139
+ <DsDropdownMenu.Content className={styles.dropdownContent}>
140
+ <DsDropdownMenu.Item value="toggle-action" onClick={onToggleActionRequired}>
141
+ {actionRequired ? 'Remove action requirement' : 'Require action'}
142
+ </DsDropdownMenu.Item>
143
+ <DsDropdownMenu.Item value="forward" onClick={onForward}>
144
+ Forward
145
+ </DsDropdownMenu.Item>
146
+ <DsDropdownMenu.Item value="mark-unread" onClick={onMarkUnread}>
147
+ Mark as &apos;Unread&apos;
148
+ </DsDropdownMenu.Item>
149
+ <DsDropdownMenu.Item value="copy-link" onClick={onCopyLink}>
150
+ Copy link
151
+ </DsDropdownMenu.Item>
152
+ <DsDropdownMenu.Item value="delete" onClick={onDelete} className={styles.danger}>
153
+ Delete
154
+ </DsDropdownMenu.Item>
155
+ </DsDropdownMenu.Content>
156
+ </DsDropdownMenu.Root>
157
+
158
+ {onResolve && (
159
+ <DsButton
160
+ design="v1.2"
161
+ buttonType="tertiary"
162
+ size="small"
163
+ onClick={() => onResolve()}
164
+ aria-label="Resolve"
165
+ className={styles.headerActionButton}
166
+ contentClassName={styles.iconButtonContent}
167
+ >
168
+ <DsIcon icon="check_circle" size="tiny" />
169
+ </DsButton>
170
+ )}
171
+ </div>
172
+
173
+ {onClose && (
174
+ <DsButton
175
+ design="v1.2"
176
+ buttonType="tertiary"
177
+ size="small"
178
+ onClick={() => onClose()}
179
+ className={styles.headerActionButton}
180
+ contentClassName={styles.iconButtonContent}
181
+ aria-label="Close"
182
+ >
183
+ <DsIcon icon="close" size="tiny" />
184
+ </DsButton>
185
+ )}
186
+ </div>
187
+ </div>
188
+ )}
189
+
190
+ {hasThread && comment && (
191
+ <DsCommentThread
192
+ messages={comment.messages}
193
+ commentAuthor={comment.author}
194
+ currentUser={currentUser}
195
+ onEditMessage={onEditMessage}
196
+ onDeleteMessage={onDeleteMessage}
197
+ onMarkUnread={onMessageMarkUnread}
198
+ onResolved={onMessageResolved}
199
+ />
200
+ )}
201
+
202
+ <div className={styles.inputArea}>
203
+ <div className={styles.inputWrapper}>
204
+ <textarea
205
+ ref={textareaRef}
206
+ className={styles.textarea}
207
+ placeholder={placeholder}
208
+ value={value}
209
+ onChange={(e) => onValueChange?.(e.target.value)}
210
+ onKeyDown={handleKeyDown}
211
+ rows={1}
212
+ aria-label={placeholder}
213
+ />
214
+ </div>
215
+
216
+ {(isInitialMode || hasThread) && (
217
+ <DsButton design="v1.2" size="small" disabled={!hasContent} onClick={handleSend} aria-label="Send">
218
+ <DsIcon icon="send" size="tiny" />
219
+ </DsButton>
220
+ )}
221
+ </div>
222
+
223
+ {showFooter && (
224
+ <div className={styles.footer}>
225
+ <DsCheckbox
226
+ label="Action required"
227
+ checked={actionRequired}
228
+ onCheckedChange={(checked) => onActionRequiredChange?.(checked === true)}
229
+ className={classNames(styles.actionRequiredCheckbox, {
230
+ [styles.checked]: actionRequired,
231
+ })}
232
+ />
233
+
234
+ <DsButton design="v1.2" size="small" disabled={!hasContent} onClick={handleSend} aria-label="Send">
235
+ <DsIcon icon="send" size="tiny" />
236
+ </DsButton>
237
+ </div>
238
+ )}
239
+ </div>
240
+ );
241
+ };