wini-web-components 0.1.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 (62) hide show
  1. package/README.md +70 -0
  2. package/package.json +62 -0
  3. package/public/favicon.ico +0 -0
  4. package/public/index.html +43 -0
  5. package/public/logo192.png +0 -0
  6. package/public/logo512.png +0 -0
  7. package/public/manifest.json +25 -0
  8. package/public/robots.txt +3 -0
  9. package/src/App.css +38 -0
  10. package/src/App.js +25 -0
  11. package/src/App.test.js +8 -0
  12. package/src/component/calendar/calendar.css +133 -0
  13. package/src/component/calendar/calendar.tsx +448 -0
  14. package/src/component/checkbox/checkbox.css +46 -0
  15. package/src/component/checkbox/checkbox.tsx +59 -0
  16. package/src/component/component-status.tsx +53 -0
  17. package/src/component/date-picker/date-picker.css +114 -0
  18. package/src/component/date-picker/date-picker.tsx +363 -0
  19. package/src/component/dialog/dialog.css +125 -0
  20. package/src/component/dialog/dialog.tsx +91 -0
  21. package/src/component/export-component.js +48 -0
  22. package/src/component/import-file/import-file.css +86 -0
  23. package/src/component/import-file/import-file.tsx +154 -0
  24. package/src/component/infinite-scroll/infinite-scroll.css +37 -0
  25. package/src/component/infinite-scroll/infinite-scroll.tsx +33 -0
  26. package/src/component/input-multi-select/input-multi-select.css +127 -0
  27. package/src/component/input-multi-select/input-multi-select.tsx +261 -0
  28. package/src/component/pagination/pagination.css +69 -0
  29. package/src/component/pagination/pagination.tsx +65 -0
  30. package/src/component/popup/popup.css +90 -0
  31. package/src/component/popup/popup.tsx +105 -0
  32. package/src/component/progress-bar/progress-bar.css +47 -0
  33. package/src/component/progress-bar/progress-bar.tsx +34 -0
  34. package/src/component/progress-circle/progress-circle.css +47 -0
  35. package/src/component/progress-circle/progress-circle.tsx +24 -0
  36. package/src/component/radio-button/radio-button.css +49 -0
  37. package/src/component/radio-button/radio-button.tsx +59 -0
  38. package/src/component/rating/rating.css +11 -0
  39. package/src/component/rating/rating.tsx +65 -0
  40. package/src/component/select1/select1.css +155 -0
  41. package/src/component/select1/select1.tsx +241 -0
  42. package/src/component/slider/slider.css +16 -0
  43. package/src/component/slider/slider.tsx +87 -0
  44. package/src/component/switch/switch.css +53 -0
  45. package/src/component/switch/switch.tsx +67 -0
  46. package/src/component/table/table.css +74 -0
  47. package/src/component/table/table.tsx +99 -0
  48. package/src/component/text/text.css +16 -0
  49. package/src/component/text/text.tsx +21 -0
  50. package/src/component/text-area/text-area.css +63 -0
  51. package/src/component/text-area/text-area.tsx +55 -0
  52. package/src/component/text-field/text-field.css +64 -0
  53. package/src/component/text-field/text-field.tsx +63 -0
  54. package/src/component/toast-noti/toast-noti.css +0 -0
  55. package/src/component/toast-noti/toast-noti.tsx +21 -0
  56. package/src/index.css +13 -0
  57. package/src/index.js +17 -0
  58. package/src/logo.svg +1 -0
  59. package/src/reportWebVitals.js +13 -0
  60. package/src/setupTests.js +5 -0
  61. package/src/skin/color.css +17 -0
  62. package/src/skin/typography.css +378 -0
@@ -0,0 +1,91 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom'
3
+ import './dialog.css'
4
+ import { ComponentStatus, getStatusIcon } from '../export-component'
5
+
6
+
7
+ export enum DialogAlignment {
8
+ start = 'start',
9
+ center = 'center',
10
+ end = 'end'
11
+ }
12
+
13
+ interface DialogState {
14
+ readonly open?: boolean,
15
+ title: string,
16
+ status: ComponentStatus,
17
+ content: string,
18
+ onSubmit: Function,
19
+ submitTitle?: string,
20
+ alignment?: DialogAlignment,
21
+ }
22
+
23
+ export const showDialog = ({ ref, title, status, content, onSubmit, submitTitle, alignment }: {
24
+ ref: React.MutableRefObject<Dialog>,
25
+ title?: string,
26
+ status?: ComponentStatus,
27
+ content?: string,
28
+ onSubmit?: Function,
29
+ submitTitle?: string,
30
+ alignment?: DialogAlignment
31
+ }) => {
32
+ ref.current.showDialogNoti({
33
+ title: title ?? '',
34
+ status: status ?? ComponentStatus.INFOR,
35
+ content: content ?? '',
36
+ onSubmit: onSubmit ?? (() => { }),
37
+ submitTitle: submitTitle,
38
+ alignment: alignment
39
+ })
40
+ }
41
+
42
+ export class Dialog extends React.Component<Object, DialogState> {
43
+ state: Readonly<DialogState> = {
44
+ open: false,
45
+ title: '',
46
+ status: ComponentStatus.INFOR,
47
+ content: '',
48
+ onSubmit: () => { }
49
+ }
50
+ showDialogNoti(data: DialogState) {
51
+ this.setState({ open: true, ...data })
52
+ }
53
+
54
+ closeDialog() {
55
+ this.setState({ open: false })
56
+ }
57
+
58
+ render() {
59
+ return (
60
+ <>
61
+ {this.state.open &&
62
+ ReactDOM.createPortal(
63
+ <div className='dialog-overlay'>
64
+ <div className='dialog-container col' style={{ width: '41.4rem', alignItems: this.state.alignment }} dialog-type={this.state.status} onClick={e => e.stopPropagation()} >
65
+ <div key={'dialog-body'} className='dialog-body col' style={{ alignItems: 'inherit' }}>
66
+ <div className='dialog-status row'>{getStatusIcon(this.state.status)}</div>
67
+ <div className='dialog-title' style={{ textAlign: this.state.alignment === DialogAlignment.center ? 'center' : 'start' }}>{this.state.title}</div>
68
+ <div className='dialog-content' style={{ textAlign: this.state.alignment === DialogAlignment.center ? 'center' : 'start' }}>{this.state.content}</div>
69
+ </div>
70
+ <div key={'dialog-footer'} className='dialog-footer row'>
71
+ <button type='button' style={this.state.alignment === DialogAlignment.center ? { flex: 1, width: '100%' } : undefined} onClick={() => this.setState({ open: false })} className='dialog-action'>Quay lại</button>
72
+ <button type='button' style={this.state.alignment === DialogAlignment.center ? { flex: 1, width: '100%' } : undefined} onClick={() => {
73
+ this.state.onSubmit();
74
+ this.setState({ open: false });
75
+ }} className='dialog-action dialog-submit' >
76
+ {this.state.submitTitle ?? 'Xác nhận'}
77
+ </button>
78
+ </div>
79
+ <button type='button' onClick={() => this.setState({ open: false })} className='dialog-close-btn row' >
80
+ <svg width='100%' height='100%' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg' style={{ width: '2rem', height: '2rem' }} >
81
+ <path fillRule='evenodd' clipRule='evenodd' d='M16.4223 4.7559C16.7477 4.43047 16.7477 3.90283 16.4223 3.57739C16.0968 3.25195 15.5692 3.25195 15.2438 3.57739L9.99967 8.82147L4.7556 3.57739C4.43016 3.25195 3.90252 3.25195 3.57709 3.57739C3.25165 3.90283 3.25165 4.43047 3.57709 4.7559L8.82116 9.99998L3.57709 15.2441C3.25165 15.5695 3.25165 16.0971 3.57709 16.4226C3.90252 16.748 4.43016 16.748 4.7556 16.4226L9.99967 11.1785L15.2438 16.4226C15.5692 16.748 16.0968 16.748 16.4223 16.4226C16.7477 16.0971 16.7477 15.5695 16.4223 15.2441L11.1782 9.99998L16.4223 4.7559Z' fill='#00204D' fillOpacity={0.6} />
82
+ </svg>
83
+ </button>
84
+ </div>
85
+ </div>,
86
+ document.body
87
+ )}
88
+ </>
89
+ )
90
+ }
91
+ }
@@ -0,0 +1,48 @@
1
+ import { Checkbox } from './checkbox/checkbox.tsx'
2
+ import { Select1 } from './select1/select1.tsx'
3
+ import { Switch } from './switch/switch.tsx'
4
+ import { showPopup, closePopup, Popup } from './popup/popup.tsx'
5
+ import { showDialog, Dialog, DialogAlignment } from './dialog/dialog.tsx'
6
+ import { DatePicker } from './date-picker/date-picker.tsx'
7
+ import { SelectMultiple } from './input-multi-select/input-multi-select.tsx'
8
+ import { ProgressBar } from './progress-bar/progress-bar.tsx'
9
+ import { ComponentStatus, getStatusIcon } from './component-status.tsx'
10
+ import { Text } from './text/text.tsx'
11
+ import { Pagination } from './pagination/pagination.tsx'
12
+ import { Table, TbCell, TbHeader, TbRow, TbBody, CellAlignItems } from './table/table.tsx'
13
+ import { TextField } from './text-field/text-field.tsx'
14
+ import { RadioButton } from './radio-button/radio-button.tsx'
15
+ import { TextArea } from './text-area/text-area.tsx'
16
+ import { ImportFile } from './import-file/import-file.tsx'
17
+ import { ToastMessage } from './toast-noti/toast-noti.tsx'
18
+ import { Calendar, CalendarType } from './calendar/calendar'
19
+ import { InfiniteScroll } from './infinite-scroll/infinite-scroll.tsx'
20
+ import { Rating } from './rating/rating.tsx'
21
+ import { ProgressCircle } from './progress-circle/progress-circle.tsx'
22
+ import { CustomSlider } from './slider/slider.tsx'
23
+
24
+ export {
25
+ Calendar, CalendarType,
26
+ ComponentStatus,
27
+ getStatusIcon,
28
+ Checkbox,
29
+ Select1,
30
+ Switch,
31
+ Popup, showPopup, closePopup,
32
+ Dialog, showDialog, DialogAlignment,
33
+ DatePicker,
34
+ SelectMultiple,
35
+ ProgressBar,
36
+ Text,
37
+ Pagination,
38
+ Table, TbCell, TbHeader, TbBody, TbRow, CellAlignItems,
39
+ TextField,
40
+ RadioButton,
41
+ TextArea,
42
+ ImportFile,
43
+ ToastMessage,
44
+ InfiniteScroll,
45
+ Rating,
46
+ ProgressCircle,
47
+ CustomSlider,
48
+ }
@@ -0,0 +1,86 @@
1
+ .import-file-container {
2
+ align-items: center;
3
+ justify-content: center;
4
+ }
5
+
6
+ .import-file-container input {
7
+ display: none;
8
+ }
9
+
10
+ .import-file-container button {
11
+ outline: none;
12
+ border: none;
13
+ background-color: transparent;
14
+ padding: 0;
15
+ }
16
+
17
+ .import-file-container:not(.button-only) {
18
+ max-width: 57rem;
19
+ padding: 2.4rem;
20
+ gap: 1.6rem;
21
+ border-radius: 0.8rem;
22
+ border: 1px solid #00358033;
23
+ }
24
+
25
+ .import-file-container:not(.button-only):has(svg.preview-icon) {
26
+ border-style: dashed;
27
+ }
28
+
29
+ .import-file-container>.import-file-prefix:not(*:has(>img)) {
30
+ width: 4.8rem;
31
+ height: 4.8rem;
32
+ border-radius: 50%;
33
+ }
34
+
35
+ .import-file-container>.import-file-prefix {
36
+ background-color: #edf2fd;
37
+ align-items: center;
38
+ justify-content: center;
39
+ }
40
+
41
+ .import-file-container>.import-file-prefix>img {
42
+ max-width: 14rem;
43
+ max-height: 8.8rem;
44
+ border-radius: 0.8rem;
45
+ }
46
+
47
+ .import-file-container>.file-preview-content {
48
+ max-width: 100%;
49
+ flex: 1;
50
+ row-gap: 0.4rem;
51
+ align-items: start;
52
+ }
53
+
54
+ .import-file-container.col>.file-preview-content {
55
+ align-items: center;
56
+ }
57
+
58
+ .import-file-container>.import-file-suffix {
59
+ padding: 0.8rem 1.6rem;
60
+ border-radius: 0.8rem;
61
+ column-gap: 0.8rem;
62
+ align-items: center;
63
+ justify-content: center;
64
+ background-color: #f2f5f8;
65
+ }
66
+
67
+ .import-file-container>.import-file-suffix>span {
68
+ padding: 0;
69
+ font: 500 1.4rem/2.2rem "Inter";
70
+ color: #00204d99;
71
+ }
72
+
73
+ .import-file-container .preview-file-link {
74
+ font: 500 1.2rem/1.6rem "Inter";
75
+ color: #366ae2;
76
+ }
77
+
78
+ .remove-preview-file {
79
+ width: 1.6rem;
80
+ height: 1.6rem;
81
+ margin-left: 0.6rem;
82
+ }
83
+
84
+ .Toastify__toast-body>div:last-child {
85
+ font: 500 1.2rem/1.6rem "Inter";
86
+ }
@@ -0,0 +1,154 @@
1
+ import React, { CSSProperties } from 'react'
2
+ import './import-file.css'
3
+ import { ComponentStatus } from '../component-status'
4
+ import { Text, ToastMessage } from '../export-component'
5
+
6
+ const cloudSvg = (
7
+ <svg width='100%' height='100%' style={{ width: '3rem', height: '3rem' }} viewBox='0 0 36 36' fill='none' xmlns='http://www.w3.org/2000/svg' >
8
+ <path d='M22.5312 6.51941C20.3258 6.12929 18.0555 6.35518 15.9702 7.1722C13.8849 7.98923 12.0654 9.36573 10.712 11.1502C9.53042 12.7081 8.74407 14.5243 8.41412 16.4432C6.99557 16.9154 5.7486 17.8144 4.85059 19.0274C3.77621 20.4786 3.27749 22.2764 3.45068 24.0737C3.62388 25.871 4.45672 27.5405 5.78845 28.7599C7.12018 29.9792 8.85639 30.6621 10.662 30.6766H13.1063C13.7786 30.6766 14.3236 30.1316 14.3236 29.4594C14.3236 28.7871 13.7786 28.2421 13.1063 28.2421H10.6769C9.47485 28.2313 8.31921 27.7762 7.43253 26.9643C6.54471 26.1514 5.98948 25.0384 5.87402 23.8402C5.75855 22.642 6.09103 21.4435 6.80729 20.476C7.52354 19.5085 8.57279 18.8406 9.75252 18.6013C10.2753 18.4952 10.6682 18.061 10.7216 17.5303C10.9012 15.7476 11.5691 14.049 12.6518 12.6214C13.7345 11.1938 15.1901 10.0926 16.8583 9.43899C18.5266 8.78536 20.3428 8.60466 22.1071 8.91675C23.8715 9.22884 25.5155 10.0216 26.8583 11.2079C28.2011 12.3941 29.1905 13.9278 29.7178 15.6402C30.2451 17.3526 30.2898 19.1772 29.8469 20.9134C29.404 22.6495 28.4907 24.2297 27.2075 25.4802C25.9244 26.7308 24.3211 27.603 22.5742 28.001C21.9187 28.1504 21.5084 28.8028 21.6577 29.4583C21.807 30.1138 22.4595 30.5241 23.115 30.3748C25.2987 29.8772 27.3028 28.7869 28.9067 27.2238C30.5107 25.6606 31.6523 23.6853 32.2059 21.5152C32.7595 19.345 32.7037 17.0642 32.0446 14.9237C31.3855 12.7833 30.1486 10.8661 28.4701 9.38333C26.7916 7.90052 24.7366 6.90953 22.5312 6.51941Z' fill='#366AE2' />
9
+ <path
10
+ d='M17.1146 17.6431C17.2313 17.5264 17.3658 17.4384 17.5094 17.379C17.6513 17.3201 17.8067 17.2874 17.9697 17.2866L17.9753 17.2866L17.9809 17.2866C18.2906 17.288 18.5998 17.4069 18.8361 17.6431L23.7052 22.5123C24.1806 22.9876 24.1806 23.7584 23.7052 24.2338C23.2298 24.7091 22.4591 24.7091 21.9837 24.2338L19.1926 21.4427V29.4594C19.1926 30.1317 18.6476 30.6767 17.9753 30.6767C17.303 30.6767 16.758 30.1317 16.758 29.4594V21.4427L13.9669 24.2338C13.4916 24.7091 12.7208 24.7091 12.2455 24.2338C11.7701 23.7584 11.7701 22.9876 12.2455 22.5123L17.1146 17.6431Z'
11
+ fill='#366AE2'
12
+ />
13
+ </svg>
14
+ )
15
+
16
+ const fileSvg = (
17
+ <svg className='preview-icon' width='100%' height='100%' style={{ width: '3rem', height: '3rem' }} viewBox='0 0 36 36' fill='none' xmlns='http://www.w3.org/2000/svg' >
18
+ <path d='M20.9163 3.41669H7.54829C7.22597 3.41669 6.91686 3.54472 6.68895 3.77263C6.46105 4.00054 6.33301 4.30965 6.33301 4.63196V31.3681C6.33301 31.6904 6.46105 31.9995 6.68895 32.2274C6.91686 32.4553 7.22597 32.5834 7.54829 32.5834H29.4233C29.7456 32.5834 30.0547 32.4553 30.2826 32.2274C30.5105 31.9995 30.6386 31.6904 30.6386 31.3681V13.1389H22.1316C21.8093 13.1389 21.5002 13.0109 21.2723 12.783C21.0444 12.5551 20.9163 12.2459 20.9163 11.9236V3.41669Z' fill='#366AE2' />
19
+ <path d='M29.9264 10.7084H23.3469V4.12884L29.9264 10.7084Z' fill='#366AE2' />
20
+ </svg>
21
+ )
22
+
23
+ const closeSvg = (
24
+ <svg width='100%' height='100%' style={{ width: '2.4rem', height: '2.4rem' }} fill='none' xmlns='http://www.w3.org/2000/svg' >
25
+ <path d='M13.4144 12.0002L20.4144 5.00015L19.0002 3.58594L12.0002 10.5859L5.00015 3.58594L3.58594 5.00015L10.5859 12.0002L3.58594 19.0002L5.00015 20.4144L12.0002 13.4144L19.0002 20.4144L20.4144 19.0002L13.4144 12.0002Z' fill='#E60012FF' />
26
+ </svg>
27
+ )
28
+
29
+ const uploadSvg = (
30
+ <svg width='100%' height='100%' style={{ width: '1.4rem', height: '1.4rem' }} fill='none' xmlns='http://www.w3.org/2000/svg' >
31
+ <path d='M3.66732 3.88889L7.00065 0L10.334 3.88889H7.55621V8.88889H6.4451V3.88889H3.66732Z' fill='#262626' />
32
+ <path d='M12.5562 11.1111H1.4451V7.77778H0.333984V11.6667C0.333984 11.9733 0.582873 12.2222 0.88954 12.2222H13.1118C13.4184 12.2222 13.6673 11.9733 13.6673 11.6667V7.77778H12.5562V11.1111Z' fill='#262626' />
33
+ </svg>
34
+ )
35
+
36
+ interface ObjWithKnownKeys {
37
+ [k: string]: any;
38
+ }
39
+
40
+ interface ImportFileState {
41
+ status?: ComponentStatus,
42
+ preview: File | ObjWithKnownKeys | undefined | null
43
+ }
44
+
45
+ type ChangeFileFunction = (a?: File) => void;
46
+
47
+ interface ImportFileProps {
48
+ status?: ComponentStatus,
49
+ value?: File | ObjWithKnownKeys,
50
+ buttonOnly: boolean,
51
+ onChange?: ChangeFileFunction,
52
+ label?: string,
53
+ className?: string,
54
+ style?: CSSProperties,
55
+ allowType?: Array<string>,
56
+ subTitle?: string,
57
+ /**
58
+ * maxSize unit: kb (kilobytes)
59
+ */
60
+ maxSize?: number
61
+ }
62
+
63
+ export class ImportFile extends React.Component<ImportFileProps, ImportFileState> {
64
+ private fileRef: React.RefObject<HTMLInputElement>;
65
+ constructor(props: ImportFileProps | Readonly<ImportFileProps>) {
66
+ super(props);
67
+ this.fileRef = React.createRef();
68
+ }
69
+
70
+ state: Readonly<ImportFileState> = {
71
+ preview: this.props.value
72
+ }
73
+
74
+ showFilePicker() {
75
+ this.fileRef.current?.click()
76
+ }
77
+
78
+ componentDidUpdate(prevProps: Readonly<ImportFileProps>, prevState: Readonly<ImportFileState>): void {
79
+ if (prevProps.value !== this.props.value || prevProps.status !== this.props.status) {
80
+ this.setState({ ...this.state, status: this.props.status, preview: this.props.value })
81
+ }
82
+ }
83
+
84
+ render() {
85
+ let sizeTitle: string | undefined
86
+ if (this.props.maxSize) {
87
+ sizeTitle = this.props.maxSize > Math.pow(1024, 3) ? `${Math.round(this.props.maxSize / Math.pow(1024, 3))}TB` : this.props.maxSize > Math.pow(1024, 2) ? `${Math.round(this.props.maxSize / Math.pow(1024, 2))}GB` : this.props.maxSize > 1024 ? `${Math.round(this.props.maxSize / 1024)}MB` : `${this.props.maxSize}KB`
88
+ }
89
+ return <div className={`import-file-container ${this.props.className ?? 'row'} ${this.props.buttonOnly ? 'button-only' : ''}`} style={this.state.preview ? this.props.style : { cursor: 'pointer', ...this.props.style }}
90
+ onClick={() => {
91
+ if (!this.state.preview && !this.props.buttonOnly) this.showFilePicker()
92
+ }}
93
+ >
94
+ <input type='file' accept={(this.props.allowType ?? []).join(',')} ref={this.fileRef} onChange={(ev) => {
95
+ let file: File | undefined
96
+ if (ev.target.files && ev.target.files[0]) {
97
+ file = ev.target.files[0]
98
+ if (this.props.maxSize) {
99
+ if (file.size > (this.props.maxSize * 1024)) {
100
+ ToastMessage.errors(`Tệp ${file.name} vượt qua kích thước cho phép`)
101
+ file = undefined
102
+ }
103
+ }
104
+ }
105
+ if (file) {
106
+ this.setState({ ...this.state, preview: file })
107
+ if (this.props.onChange) this.props.onChange(file)
108
+ }
109
+ }} />
110
+ {this.props.buttonOnly
111
+ ? null
112
+ : [
113
+ <div key={'import-file-prefix'} className='import-file-prefix row'>
114
+ {this.state.preview ? this.state.preview.type?.includes('image') ? <img src={this.state.preview instanceof File ? URL.createObjectURL(this.state.preview) : this.state.preview.url} /> : fileSvg : cloudSvg}
115
+ </div>,
116
+ <div key={'file-preview-content'} className='file-preview-content col' >
117
+ <Text className='title-file heading-8' style={{ maxWidth: '100%' }}>
118
+ {this.state.preview?.name ?? (this.props.label ?? 'Nhấp hoặc kéo tệp vào đây để tải lên')}
119
+ </Text>
120
+ <Text className='subtitle-file subtitle-3' style={{ maxWidth: '100%' }}>
121
+ {this.state.preview?.size
122
+ ? `${this.state.preview?.size}KB`
123
+ : (this.props.subTitle ?? (sizeTitle ? `Kích thước tệp nhỏ hơn ${sizeTitle}.` : ''))}
124
+ </Text>
125
+ </div>
126
+ ]}
127
+ {this.state.preview && this.props.buttonOnly ? (
128
+ <div className='row' style={{}}>
129
+ <Text className='preview-file-link'>
130
+ {this.state.preview?.name ?? ''}
131
+ </Text>
132
+ <button type='button' className='remove-preview-file' onClick={() => {
133
+ this.setState({ ...this.state, preview: null })
134
+ }}
135
+ >
136
+ {closeSvg}
137
+ </button>
138
+ </div>
139
+ ) : (
140
+ <button
141
+ type='button'
142
+ className='import-file-suffix row'
143
+ onClick={() => {
144
+ if (this.props.buttonOnly || this.state.preview) this.showFilePicker()
145
+ }}
146
+ >
147
+ {this.props.buttonOnly ? uploadSvg : null}
148
+ <span>{this.state.preview ? 'Thay tệp' : 'Chọn tệp'}</span>
149
+ </button>
150
+ )}
151
+ </div>
152
+ }
153
+ }
154
+
@@ -0,0 +1,37 @@
1
+ .infinite-scroll.loading {
2
+ position: relative;
3
+ }
4
+
5
+ .infinite-scroll.loading::after {
6
+ content: '';
7
+ position: absolute;
8
+ width: 1.2rem;
9
+ height: 1.2rem;
10
+ border: 0.4rem solid #f3f3f3;
11
+ border-radius: 50%;
12
+ border-top: 0.4rem solid var(--infor-color);
13
+ -webkit-animation: spin 2s linear infinite;
14
+ /* Safari */
15
+ animation: spin 2s linear infinite;
16
+ }
17
+
18
+ /* Safari */
19
+ @-webkit-keyframes spin {
20
+ 0% {
21
+ -webkit-transform: rotate(0deg);
22
+ }
23
+
24
+ 100% {
25
+ -webkit-transform: rotate(360deg);
26
+ }
27
+ }
28
+
29
+ @keyframes spin {
30
+ 0% {
31
+ transform: rotate(0deg);
32
+ }
33
+
34
+ 100% {
35
+ transform: rotate(360deg);
36
+ }
37
+ }
@@ -0,0 +1,33 @@
1
+ import React, { CSSProperties, ReactNode } from 'react';
2
+
3
+
4
+ interface InfiniteScrollProps {
5
+ className?: string,
6
+ style?: CSSProperties,
7
+ handleScroll?: (onLoadMore: boolean, ev: React.UIEvent<HTMLDivElement, UIEvent>) => Promise<any> | null,
8
+ children?: ReactNode,
9
+ totalCount?: number,
10
+ }
11
+
12
+ interface InfiniteScrollState {
13
+ loading: boolean
14
+ }
15
+
16
+ export class InfiniteScroll extends React.Component<InfiniteScrollProps, InfiniteScrollState> {
17
+ state: Readonly<InfiniteScrollState> = {
18
+ loading: false
19
+ }
20
+
21
+ render() {
22
+ return <div onScroll={async (ev) => {
23
+ if (this.props.handleScroll) {
24
+ this.setState({ ...this.state, loading: true })
25
+ let scrollElement = ev.target as HTMLDivElement
26
+ await this.props.handleScroll(Math.round(scrollElement.offsetHeight + scrollElement.scrollTop) >= (scrollElement.scrollHeight - 1), ev)
27
+ this.setState({ loading: false })
28
+ }
29
+ }} className={`infinite-scroll ${this.state.loading ? 'loading' : ''} ${this.props.className ?? 'col'}`} style={this.props.style ?? { 'overflow': 'hidden auto' }}>
30
+ {this.props.children}
31
+ </div>
32
+ }
33
+ }
@@ -0,0 +1,127 @@
1
+ .select-multi-container {
2
+ border-radius: 0.4rem;
3
+ align-items: center;
4
+ justify-content: start;
5
+ width: 100%;
6
+ height: 4rem;
7
+ max-height: 100%;
8
+ box-sizing: border-box;
9
+ padding: 0.8rem;
10
+ gap: 0.8rem;
11
+ width: 16rem;
12
+ position: relative;
13
+ border: 0.5px solid #00358033;
14
+ border-radius: 4px;
15
+ cursor: context-menu;
16
+ }
17
+
18
+ .select-multi-container.disabled {
19
+ background-color: var(--disabled-background) !important;
20
+ pointer-events: none !important;
21
+ }
22
+
23
+ .select-multi-container.helper-text {
24
+ overflow: visible !important;
25
+ border-color: var(--helper-text-color) !important;
26
+ }
27
+
28
+ .select-multi-container.helper-text::after {
29
+ content: attr(helper-text);
30
+ color: var(--helper-text-color);
31
+ position: absolute;
32
+ left: 0;
33
+ bottom: -4px;
34
+ width: max-content;
35
+ font: 400 1.2rem/1.6rem 'Inter';
36
+ transform: translate(-7.5%, 92.5%) scale(0.85);
37
+ }
38
+
39
+ .select-multi-container>.row::-webkit-scrollbar {
40
+ display: none;
41
+ }
42
+
43
+ .selected-item-value>* {
44
+ pointer-events: none;
45
+ }
46
+
47
+ .select-multi-container .select-multi-placeholder {
48
+ font: 400 1.4rem/2.2rem "Inter";
49
+ color: #c5c5c5;
50
+ }
51
+
52
+ .select-multi-container .selected-item-value {
53
+ border-radius: 10rem;
54
+ padding: 0.4rem 0.8rem;
55
+ background-color: var(--selected-background);
56
+ column-gap: 0.8rem;
57
+ }
58
+
59
+ .select-multi-container .selected-item-value>.selected-value-title {
60
+ font: 400 1.2rem/1.6rem "Inter";
61
+ color: #161D24E5;
62
+ }
63
+
64
+ .select-multi-popup {
65
+ position: absolute;
66
+ z-index: 9999;
67
+ background-color: #fff;
68
+ border-radius: 0.8rem;
69
+ height: fit-content;
70
+ box-shadow: 0 0 12px 0 #00204d1f;
71
+ align-items: center;
72
+ padding: 0.8rem 0;
73
+ box-shadow: -20px 20px 40px -4px rgba(145, 158, 171, 0.24), 0px 0px 2px 0px rgba(145, 158, 171, 0.24);
74
+ }
75
+
76
+ .select-multi-popup>.header-search {
77
+ width: 100%;
78
+ padding: 0.8rem 1.6rem;
79
+ box-sizing: border-box;
80
+ align-items: center;
81
+ }
82
+
83
+ .select-multi-popup>.header-search>input {
84
+ outline: none;
85
+ padding: 0;
86
+ border: 0;
87
+ font: 400 1.4rem/2.2rem "Inter";
88
+ color: #1C2430;
89
+ min-width: 0;
90
+ flex: 1;
91
+ max-width: 100%;
92
+ }
93
+
94
+ .select-multi-popup>.select-body {
95
+ max-height: 100%;
96
+ flex: 1;
97
+ align-items: start;
98
+ row-gap: 2px;
99
+ max-height: 28rem;
100
+ min-height: 8rem;
101
+ width: 100%;
102
+ padding-top: 0.4rem;
103
+ overflow: hidden auto;
104
+ }
105
+
106
+ .select-multi-popup>.select-body>.select-tile {
107
+ justify-content: start;
108
+ align-items: center;
109
+ padding: 0.8rem 1.6rem;
110
+ gap: 0.8rem;
111
+ width: 100%;
112
+ cursor: context-menu;
113
+ }
114
+
115
+ .select-multi-popup>.select-body>.select-tile:hover {
116
+ background-color: var(--hover-background);
117
+ }
118
+
119
+ .select-multi-popup>.select-body>.select-tile:has(input:checked) {
120
+ background-color: var(--selected-background);
121
+ }
122
+
123
+ .no-results-found {
124
+ padding: 0.8rem;
125
+ font: 400 1.4rem/2rem "Inter";
126
+ color: #1C2430;
127
+ }