@soul-refiner/design-system 0.1.12 → 0.1.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soul-refiner/design-system",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "main": "src/index.ts",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -173,8 +173,6 @@ const base = css<{
173
173
  color: var(--color-grey-7) !important;
174
174
  cursor: unset !important;
175
175
  }
176
-
177
- ${({ $sx }) => ($sx ? $sx : '')}
178
176
  `;
179
177
 
180
178
  const Primary = styled(Box)<{
@@ -199,6 +197,8 @@ const Primary = styled(Box)<{
199
197
  ${base}
200
198
  ${Styles.flex}
201
199
  ${Styles.padding}
200
+
201
+ ${({ $sx }) => ($sx ? $sx : '')}
202
202
  `;
203
203
 
204
204
  const Secondary = styled(Box)<{
@@ -222,6 +222,8 @@ const Secondary = styled(Box)<{
222
222
  ${base}
223
223
  ${Styles.flex}
224
224
  ${Styles.padding}
225
+
226
+ ${({ $sx }) => ($sx ? $sx : '')}
225
227
  `;
226
228
 
227
229
  const Outlined = styled(Box)<{
@@ -240,6 +242,8 @@ const Outlined = styled(Box)<{
240
242
  ${base}
241
243
  ${Styles.flex}
242
244
  ${Styles.padding}
245
+
246
+ ${({ $sx }) => ($sx ? $sx : '')}
243
247
  `;
244
248
 
245
249
  const Danger = styled(Box)<{
@@ -264,6 +268,8 @@ const Danger = styled(Box)<{
264
268
  ${base}
265
269
  ${Styles.flex}
266
270
  ${Styles.padding}
271
+
272
+ ${({ $sx }) => ($sx ? $sx : '')}
267
273
  `;
268
274
 
269
275
  const Text = styled(Box)<{
@@ -291,6 +297,8 @@ const Text = styled(Box)<{
291
297
  ${base}
292
298
  ${Styles.flex}
293
299
  ${Styles.padding}
300
+
301
+ ${({ $sx }) => ($sx ? $sx : '')}
294
302
  `;
295
303
 
296
304
  const Raised = styled(Box)<{
@@ -320,6 +328,8 @@ const Raised = styled(Box)<{
320
328
  ${base}
321
329
  ${Styles.flex}
322
330
  ${Styles.padding}
331
+
332
+ ${({ $sx }) => ($sx ? $sx : '')}
323
333
  `;
324
334
 
325
335
  const Special = styled(Box)<{
@@ -347,4 +357,6 @@ const Special = styled(Box)<{
347
357
  ${base}
348
358
  ${Styles.flex}
349
359
  ${Styles.padding}
360
+
361
+ ${({ $sx }) => ($sx ? $sx : '')}
350
362
  `;
@@ -119,7 +119,7 @@ const Dropdown = ({
119
119
  const handleChange = multiple ? handleChangeMultiple : handleChangeSingle;
120
120
 
121
121
  const isSelected = (value: string) =>
122
- maintainState && multiple ? selected.includes(value) : value === selected;
122
+ maintainState && (multiple ? selected.includes(value) : value === selected);
123
123
 
124
124
  /**
125
125
  * allow for the API to accept children as options
@@ -30,6 +30,7 @@ export interface IconProps extends NativeProps {
30
30
  as?: React.ElementType;
31
31
  color?: (typeof COLORS)[number];
32
32
  size?: 16 | 20 | 24;
33
+ sx?: string;
33
34
  }
34
35
 
35
36
  export const Icon = ({
@@ -37,6 +38,7 @@ export const Icon = ({
37
38
  children,
38
39
  color = 'grey-3',
39
40
  size = 16,
41
+ sx,
40
42
  ...rest
41
43
  }: IconProps) => {
42
44
  return (
@@ -47,6 +49,7 @@ export const Icon = ({
47
49
  viewBox="0 0 16 16"
48
50
  fill={colorReference[color]}
49
51
  xmlns="http://www.w3.org/2000/svg"
52
+ $sx={sx}
50
53
  {...rest}
51
54
  >
52
55
  {children}
@@ -54,4 +57,6 @@ export const Icon = ({
54
57
  );
55
58
  };
56
59
 
57
- const SVG = styled.svg``;
60
+ const SVG = styled.svg<{ $sx?: string }>`
61
+ ${({ $sx }) => ($sx ? $sx : '')}
62
+ `;
@@ -4,3 +4,5 @@ export { Filter } from './filter';
4
4
  export type { FilterProps, FilterTriggerProps } from './filter';
5
5
  export { Radio } from './radio';
6
6
  export type { RadioProps } from './radio';
7
+ export { Search } from './search';
8
+ export type { SearchProps } from './search';
@@ -0,0 +1,2 @@
1
+ export { Search } from './search';
2
+ export type { SearchProps } from './search';
@@ -0,0 +1,223 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { Styles } from 'common';
4
+ import { BoxProps } from 'components';
5
+ import { Close, Search as SearchIcon } from 'icons';
6
+
7
+ type NativeProps = Pick<
8
+ React.HTMLAttributes<HTMLInputElement>,
9
+ 'placeholder' | 'onChange' | 'onSubmit'
10
+ >;
11
+
12
+ type SearchSize = 'collapsed' | 'small' | 'medium' | 'large';
13
+
14
+ export interface SearchProps extends NativeProps {
15
+ dropdownFilter?: React.ReactElement;
16
+ onClearText?: () => void;
17
+ size: SearchSize;
18
+ sx?: string;
19
+ value: string;
20
+ }
21
+
22
+ export const Search = ({
23
+ dropdownFilter,
24
+ onChange,
25
+ onClearText,
26
+ onSubmit,
27
+ placeholder,
28
+ size,
29
+ sx,
30
+ value,
31
+ }: SearchProps) => {
32
+ const [expanded, setExpanded] = React.useState(false);
33
+ const [focused, setFocused] = React.useState(false);
34
+ const inputRef = React.useRef<HTMLInputElement>(null);
35
+
36
+ const isCollapsed = !expanded && size === 'collapsed';
37
+ const iconSize = isCollapsed || size === 'large' ? 24 : 16;
38
+
39
+ const handleExpand = () => {
40
+ setExpanded(true);
41
+ inputRef?.current?.focus();
42
+ };
43
+
44
+ const handleCollapse = () => {
45
+ setExpanded(false);
46
+ setFocused(false);
47
+ inputRef?.current?.blur();
48
+ };
49
+
50
+ const handleFocus = () => {
51
+ inputRef?.current?.focus();
52
+ };
53
+
54
+ return (
55
+ <Wrapper
56
+ $align="center"
57
+ $dir="row"
58
+ $isCollapsed={isCollapsed}
59
+ $isFocused={focused}
60
+ $borderRadius="large"
61
+ $size={size}
62
+ $sx={sx}
63
+ onClick={
64
+ dropdownFilter && size === 'large'
65
+ ? undefined
66
+ : isCollapsed
67
+ ? handleExpand
68
+ : handleFocus
69
+ }
70
+ >
71
+ <SearchIcon
72
+ color={size === 'large' ? 'blue-7' : 'grey-3'}
73
+ size={iconSize}
74
+ onClick={isCollapsed ? undefined : handleCollapse}
75
+ sx={
76
+ isCollapsed || size !== 'collapsed'
77
+ ? undefined
78
+ : `
79
+ &:hover {
80
+ cursor: pointer;
81
+ background-color: var(--color-grey-10);
82
+ border-radius: 50%;
83
+ }`
84
+ }
85
+ />
86
+ <Input
87
+ $isCollapsed={isCollapsed}
88
+ onBlur={() => setFocused(false)}
89
+ onChange={onChange}
90
+ onFocus={() => setFocused(true)}
91
+ onSubmit={onSubmit}
92
+ placeholder={placeholder}
93
+ ref={inputRef}
94
+ value={value}
95
+ />
96
+ {!isCollapsed && onClearText && (
97
+ <Close
98
+ size={16}
99
+ onClick={onClearText}
100
+ sx={`
101
+ ${value.length === 0 ? 'visibility: hidden;' : ''}
102
+ padding: 4px;
103
+ &:hover {
104
+ cursor: pointer;
105
+ background-color: var(--color-grey-10);
106
+ border-radius: 50%;
107
+ }`}
108
+ />
109
+ )}
110
+ {size === 'large' && dropdownFilter && (
111
+ <>
112
+ <Separator />
113
+ {dropdownFilter}
114
+ </>
115
+ )}
116
+ </Wrapper>
117
+ );
118
+ };
119
+
120
+ const Wrapper = styled.div<
121
+ BoxProps & {
122
+ $isCollapsed: boolean;
123
+ $isFocused: boolean;
124
+ $size: SearchSize;
125
+ $sx?: string;
126
+ }
127
+ >`
128
+ ${Styles.flex}
129
+ ${Styles.padding}
130
+ display: flex;
131
+ background-color: var(--color-white);
132
+
133
+ & > * {
134
+ width: fit-content;
135
+ flex-shrink: 0;
136
+ }
137
+
138
+ ${({ $isCollapsed, $size }) =>
139
+ $isCollapsed
140
+ ? `
141
+ padding: 12px;
142
+ border-radius: 50%;
143
+ &:hover {
144
+ background-color: var(--color-grey-10);
145
+ cursor: pointer;
146
+ }`
147
+ : `
148
+ border-radius: 8px;
149
+ ${
150
+ $size === 'collapsed'
151
+ ? // collapsed
152
+ `
153
+ gap: 8px;
154
+ padding: 10px 16px;
155
+ border: 1px solid var(--color-grey-8);
156
+
157
+ &:hover {
158
+ border: 1px solid var(--color-grey-6);
159
+ }`
160
+ : $size === 'small'
161
+ ? // small
162
+ `
163
+ gap: 12px;
164
+ padding: 10px 16px;
165
+ border: 1px solid var(--color-grey-8);
166
+
167
+ &:hover {
168
+ border: 1px solid var(--color-grey-6);
169
+ }`
170
+ : $size === 'medium'
171
+ ? // medium
172
+ `
173
+ gap: 12px;
174
+ padding: 14px 16px;
175
+ border: 1px solid var(--color-grey-8);
176
+
177
+ &:hover {
178
+ border: 1px solid var(--color-grey-6);
179
+ }`
180
+ : // large
181
+ `
182
+ gap: 16px;
183
+ padding: 16px;
184
+ box-shadow: var(--elevation-1);
185
+
186
+ &:hover {
187
+ border: 1px solid var(--color-grey-6);
188
+ }`
189
+ }`}
190
+
191
+ ${({ $isFocused, $isCollapsed }) =>
192
+ $isFocused && !$isCollapsed
193
+ ? 'border: 1px solid var(--color-primary-blue-2) !important;'
194
+ : ''}
195
+
196
+ ${({ $sx }) => ($sx ? $sx : '')}
197
+ `;
198
+
199
+ const Input = styled.input.attrs(({ ref }) => ({
200
+ ref: ref,
201
+ }))<{
202
+ $isCollapsed: boolean;
203
+ }>`
204
+ flex: 1;
205
+ border-radius: 8px;
206
+ outline: none;
207
+ border: none;
208
+ font-size: 14px;
209
+ font-family: Inter;
210
+ line-height: 20px;
211
+ color: var(--color-grey-2);
212
+
213
+ ${({ $isCollapsed }) =>
214
+ $isCollapsed
215
+ ? 'width: 0px; height: 0px; padding: 0px !important; border: none !important;'
216
+ : ''}
217
+ `;
218
+
219
+ const Separator = styled.div`
220
+ width: 1px;
221
+ height: 20px;
222
+ background-color: var(--color-grey-9);
223
+ `;
@@ -0,0 +1,243 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import Picker from '@emoji-mart/react';
4
+ import { Avatar, Box, BoxProps, P } from 'components';
5
+ import emojis from 'emoji/emojis.json';
6
+ import { ThumbsUp, Comment as CommentIcon } from 'icons';
7
+ import { abbrTimeBeforeDate } from './helpers';
8
+ import { Styles } from 'common';
9
+
10
+ export interface CommentProps {
11
+ content: string;
12
+ createdDate: Date;
13
+ onCommentClick: () => void;
14
+ onReact: (reaction: string) => void;
15
+ reactions?: Record<string, number>;
16
+ replies?: CommentProps[];
17
+ userImageUri: string;
18
+ userName: string;
19
+ userReactions?: string[];
20
+ userProfileUri: string;
21
+ }
22
+
23
+ export const Comment = ({
24
+ content,
25
+ createdDate,
26
+ onCommentClick,
27
+ onReact,
28
+ reactions,
29
+ replies,
30
+ userImageUri,
31
+ userName,
32
+ userReactions,
33
+ userProfileUri,
34
+ }: CommentProps) => {
35
+ const id = React.useId();
36
+ const [showPicker, setShowPicker] = React.useState(false);
37
+
38
+ const handleReaction = ({ native }: Record<string, string>) => {
39
+ onReact(native);
40
+ setShowPicker(false);
41
+ };
42
+
43
+ const handlePicker = (event: React.MouseEvent) => {
44
+ event.stopPropagation();
45
+ setShowPicker((prev) => !prev);
46
+ };
47
+
48
+ const handlePickerBlur = () => setShowPicker(false);
49
+
50
+ const createdTime = abbrTimeBeforeDate(createdDate, { message: true });
51
+
52
+ const reactionElements =
53
+ reactions &&
54
+ Object.entries(reactions)
55
+ .sort((a, b) => (a[1] < b[1] ? 1 : -1))
56
+ .map((reaction) => (
57
+ <Reaction
58
+ key={id + reaction[0]}
59
+ $align="center"
60
+ $dir="row"
61
+ $gap={5}
62
+ $userSelected={userReactions?.includes(reaction[0])}
63
+ >
64
+ {reaction[0]}
65
+ <P $bold $fontSize={10} $lineHeight={14}>
66
+ {reaction[1]}
67
+ </P>
68
+ </Reaction>
69
+ ));
70
+
71
+ const hasReplies = !!replies?.length && replies.length > 0;
72
+ const replyElements =
73
+ hasReplies &&
74
+ replies.map((reply, index) => (
75
+ <ReplyWrapper
76
+ $isNotLastReply={index !== replies.length - 1}
77
+ key={id + reply.createdDate.getTime().toString(36)}
78
+ >
79
+ <Comment {...reply} />
80
+ <ReplyIndicator />
81
+ </ReplyWrapper>
82
+ ));
83
+
84
+ return (
85
+ <Wrapper>
86
+ <Avatar href={userProfileUri} image={userImageUri} size={32} />
87
+ <Box $flex={1} $dir="column">
88
+ <ContentWrapper $hasReplies={hasReplies}>
89
+ <CommentBox>
90
+ <P $bold $color="grey-2" $fontSize={14} $lineHeight={20}>
91
+ {userName}
92
+ </P>
93
+ <P sx="letter-spacing: -0.176px;">{content}</P>
94
+ </CommentBox>
95
+ <Box
96
+ $align="flex-start"
97
+ $dir="row"
98
+ $display="flex"
99
+ $gap={16}
100
+ sx="margin: 8px 0px; position: relative;"
101
+ >
102
+ <ThumbsUp
103
+ color="primary-blue-2"
104
+ onClick={handlePicker}
105
+ size={24}
106
+ sx="border-radius: 50%; cursor: pointer; flex-shrink: 0;"
107
+ />
108
+ <CommentIcon
109
+ color="primary-blue-2"
110
+ onClick={onCommentClick}
111
+ size={24}
112
+ sx="border-radius: 50%; cursor: pointer; flex-shrink: 0;"
113
+ />
114
+ <P $color="grey-7">•</P>
115
+ <P
116
+ $color="grey-4"
117
+ $fontSize={12}
118
+ $lineHeight={24}
119
+ sx="flex-shrink: 0;"
120
+ >
121
+ {createdTime}
122
+ </P>
123
+ <Box
124
+ $display="flex"
125
+ $flow="row wrap"
126
+ $gap={6}
127
+ sx="max-width: calc(100% - 190px); margin-left: auto;"
128
+ >
129
+ {reactionElements}
130
+ </Box>
131
+ {showPicker && (
132
+ <PickerWrapper>
133
+ <Picker
134
+ data={emojis}
135
+ onEmojiSelect={handleReaction}
136
+ onClickOutside={handlePickerBlur}
137
+ maxFrequentRows={1}
138
+ navPosition="none"
139
+ previewPosition="none"
140
+ perLine={9}
141
+ theme="auto"
142
+ />
143
+ </PickerWrapper>
144
+ )}
145
+ </Box>
146
+ </ContentWrapper>
147
+ {hasReplies && <RepliesWrapper>{replyElements}</RepliesWrapper>}
148
+ </Box>
149
+ </Wrapper>
150
+ );
151
+ };
152
+
153
+ const Wrapper = styled.div`
154
+ max-width: 700px;
155
+ display: flex;
156
+ flex-direction: row;
157
+ align-items: flex-start;
158
+ gap: 12px;
159
+ justify-content: space-between;
160
+ position: relative;
161
+ `;
162
+
163
+ const ContentWrapper = styled.div<{
164
+ $hasReplies: boolean;
165
+ }>`
166
+ display: flex;
167
+ flex-direction: column;
168
+ ${({ $hasReplies }) =>
169
+ $hasReplies
170
+ ? `
171
+ position: relative;
172
+
173
+ &::before {
174
+ content: "";
175
+ position: absolute;
176
+ top: 32px;
177
+ left: -28px;
178
+ height: calc(100% - 12px);
179
+ border-left: 2px solid var(--color-grey-9);
180
+ }`
181
+ : ''}
182
+ `;
183
+
184
+ const CommentBox = styled.div`
185
+ padding: 16px;
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 8px;
189
+ background-color: var(--color-grey-10);
190
+ border-radius: 16px;
191
+ `;
192
+
193
+ const PickerWrapper = styled.div`
194
+ position: absolute;
195
+ z-index: 5;
196
+ `;
197
+
198
+ const Reaction = styled.div<BoxProps & { $userSelected?: boolean }>`
199
+ ${Styles.flex}
200
+ display: flex;
201
+ padding: 2px 6px;
202
+ border-radius: 12px;
203
+ ${({ $userSelected }) =>
204
+ $userSelected
205
+ ? `
206
+ background-color: var(--color-primary-blue-4);
207
+ border: 1px solid var(--color-primary-blue-3);
208
+ `
209
+ : ''}
210
+ `;
211
+
212
+ const RepliesWrapper = styled.div`
213
+ padding-top: 12px;
214
+ `;
215
+
216
+ const ReplyWrapper = styled.div<{
217
+ $isNotLastReply?: boolean;
218
+ }>`
219
+ position: relative;
220
+ ${({ $isNotLastReply }) =>
221
+ $isNotLastReply
222
+ ? `
223
+ &::before {
224
+ content: "";
225
+ position: absolute;
226
+ left: -28px;
227
+ height: 100%;
228
+ border-left: 2px solid var(--color-grey-9);
229
+ }`
230
+ : ''}
231
+ `;
232
+
233
+ const ReplyIndicator = styled.div`
234
+ position: absolute;
235
+ top: 0;
236
+ left: -28px;
237
+ width: 28px;
238
+ height: 16px;
239
+ border-bottom-left-radius: 16px;
240
+ border: 2px solid var(--color-grey-9);
241
+ border-top: none;
242
+ border-right: none;
243
+ `;
@@ -0,0 +1,35 @@
1
+ type Options = {
2
+ message?: boolean;
3
+ };
4
+
5
+ export const abbrTimeBeforeDate = (date: Date, options?: Options) => {
6
+ const { message } = options ?? {};
7
+ const now = new Date();
8
+ const diff = now.getTime() - date.getTime();
9
+
10
+ if (diff < 60000) {
11
+ // under 1 min
12
+ if (message) return 'Now';
13
+ return 'Just now';
14
+ } else if (diff < 3600000) {
15
+ // under one hour
16
+ const minutes = Math.floor(diff / 60000);
17
+ if (message) return `${minutes}m`;
18
+ return `${minutes} ${minutes === 1 ? 'min' : 'mins'}`;
19
+ } else if (diff < 86400000) {
20
+ // under one day
21
+ const hours = Math.floor(diff / 3600000);
22
+ if (message) return `${hours}h`;
23
+ return `${hours} ${hours === 1 ? 'hour' : 'hours'}`;
24
+ } else if (diff < 604800000) {
25
+ // under one week
26
+ const days = Math.floor(diff / 86400000);
27
+ if (message) return `${days}d`;
28
+ return `${days} ${days === 1 ? 'day' : 'days'}`;
29
+ } else {
30
+ if (message) {
31
+ return `${Math.floor(diff / 604800000)}w`;
32
+ }
33
+ return date.toDateString().slice(4);
34
+ }
35
+ };
@@ -0,0 +1,2 @@
1
+ export { Comment } from './comment';
2
+ export type { CommentProps } from './comment';
@@ -1,3 +1,5 @@
1
+ export { Comment } from './comment';
2
+ export type { CommentProps } from './comment';
1
3
  export { Message } from './message';
2
4
  export type { MessageProps } from './message';
3
5
  export { Modal } from './modal';