wini-web-components 2.8.2 → 2.8.5

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 (66) hide show
  1. package/dist/index.js.js +10 -10
  2. package/dist/index.js.mjs +198 -185
  3. package/package.json +6 -2
  4. package/src/component/button/button.module.css +210 -0
  5. package/src/component/button/button.tsx +57 -0
  6. package/src/component/calendar/calendar.module.css +153 -0
  7. package/src/component/calendar/calendar.tsx +389 -0
  8. package/src/component/carousel/carousel.css +622 -0
  9. package/src/component/carousel/carousel.tsx +91 -0
  10. package/src/component/checkbox/checkbox.module.css +48 -0
  11. package/src/component/checkbox/checkbox.tsx +80 -0
  12. package/src/component/ck-editor/ck-editor.css +206 -0
  13. package/src/component/ck-editor/ckeditor.tsx +522 -0
  14. package/src/component/component-status.tsx +53 -0
  15. package/src/component/date-time-picker/date-time-picker.module.css +94 -0
  16. package/src/component/date-time-picker/date-time-picker.tsx +663 -0
  17. package/src/component/dialog/dialog.module.css +111 -0
  18. package/src/component/dialog/dialog.tsx +109 -0
  19. package/src/component/import-file/import-file.module.css +83 -0
  20. package/src/component/import-file/import-file.tsx +174 -0
  21. package/src/component/infinite-scroll/infinite-scroll.module.css +34 -0
  22. package/src/component/infinite-scroll/infinite-scroll.tsx +35 -0
  23. package/src/component/input-multi-select/input-multi-select.module.css +121 -0
  24. package/src/component/input-multi-select/input-multi-select.tsx +263 -0
  25. package/src/component/input-otp/input-otp.module.css +41 -0
  26. package/src/component/input-otp/input-otp.tsx +110 -0
  27. package/src/component/number-picker/number-picker.module.css +137 -0
  28. package/src/component/number-picker/number-picker.tsx +107 -0
  29. package/src/component/pagination/pagination.module.css +48 -0
  30. package/src/component/pagination/pagination.tsx +88 -0
  31. package/src/component/popup/popup.css +136 -0
  32. package/src/component/popup/popup.tsx +125 -0
  33. package/src/component/progress-bar/progress-bar.module.css +42 -0
  34. package/src/component/progress-bar/progress-bar.tsx +33 -0
  35. package/src/component/progress-circle/progress-circle.css +0 -0
  36. package/src/component/progress-circle/progress-circle.tsx +25 -0
  37. package/src/component/radio-button/radio-button.module.css +51 -0
  38. package/src/component/radio-button/radio-button.tsx +60 -0
  39. package/src/component/rating/rating.module.css +11 -0
  40. package/src/component/rating/rating.tsx +65 -0
  41. package/src/component/select1/select1.module.css +108 -0
  42. package/src/component/select1/select1.tsx +271 -0
  43. package/src/component/switch/switch.module.css +53 -0
  44. package/src/component/switch/switch.tsx +68 -0
  45. package/src/component/table/table.css +74 -0
  46. package/src/component/table/table.tsx +108 -0
  47. package/src/component/tag/tag.module.css +108 -0
  48. package/src/component/tag/tag.tsx +31 -0
  49. package/src/component/text/text.css +27 -0
  50. package/src/component/text/text.tsx +24 -0
  51. package/src/component/text-area/text-area.module.css +57 -0
  52. package/src/component/text-area/text-area.tsx +65 -0
  53. package/src/component/text-field/text-field.module.css +71 -0
  54. package/src/component/text-field/text-field.tsx +102 -0
  55. package/src/component/toast-noti/toast-noti.css +866 -0
  56. package/src/component/toast-noti/toast-noti.tsx +22 -0
  57. package/src/component/wini-icon/winicon.module.css +110 -0
  58. package/src/component/wini-icon/winicon.tsx +9424 -0
  59. package/src/form/login/view.module.css +80 -0
  60. package/src/form/login/view.tsx +138 -0
  61. package/src/index.tsx +66 -0
  62. package/src/language/i18n.tsx +143 -0
  63. package/src/skin/layout.css +649 -0
  64. package/src/skin/root.css +294 -0
  65. package/src/skin/typography.css +314 -0
  66. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,263 @@
1
+ import React, { createRef, CSSProperties } from 'react'
2
+ import styles from './input-multi-select.module.css'
3
+ import { OptionsItem } from '../select1/select1'
4
+ import { Checkbox } from '../checkbox/checkbox'
5
+ import { Text } from '../text/text'
6
+ import { Winicon } from '../wini-icon/winicon'
7
+ import { WithTranslation, withTranslation } from 'react-i18next';
8
+ import { PopupOverlay } from '../popup/popup'
9
+
10
+ interface SelectMultipleProps extends WithTranslation {
11
+ id?: string,
12
+ value?: Array<string | number>,
13
+ options: Required<Array<OptionsItem>>,
14
+ onChange?: (value?: Array<string | number>) => void,
15
+ placeholder?: string,
16
+ disabled?: boolean,
17
+ className?: string,
18
+ helperText?: string,
19
+ helperTextColor?: string,
20
+ style?: CSSProperties,
21
+ handleSearch?: (e: string) => Promise<Array<OptionsItem>>,
22
+ handleLoadmore?: (onLoadMore: boolean, ev: React.UIEvent<HTMLDivElement, UIEvent>) => void,
23
+ showClearValueButton?: boolean,
24
+ popupClassName?: string
25
+ }
26
+
27
+ interface SelectMultipleState {
28
+ value: Array<string | number>,
29
+ options: Required<Array<OptionsItem>>,
30
+ offset: DOMRect,
31
+ isOpen: boolean,
32
+ onSelect: any,
33
+ search?: Array<OptionsItem>,
34
+ style?: Object
35
+ };
36
+
37
+ class TSelectMultiple extends React.Component<SelectMultipleProps, SelectMultipleState> {
38
+ private containerRef = createRef<HTMLDivElement>()
39
+ private inputRef = createRef<HTMLInputElement>()
40
+ constructor(props: SelectMultipleProps) {
41
+ super(props)
42
+ this.state = {
43
+ value: props.value ?? [],
44
+ options: props.options,
45
+ offset: {
46
+ x: 0,
47
+ y: 0,
48
+ height: 0,
49
+ width: 0,
50
+ bottom: 0,
51
+ left: 0,
52
+ right: 0,
53
+ top: 0,
54
+ toJSON: function () {
55
+ throw new Error('Function not implemented.')
56
+ }
57
+ },
58
+ isOpen: false,
59
+ onSelect: null,
60
+ }
61
+ this.onCheck = this.onCheck.bind(this)
62
+ this.search = this.search.bind(this)
63
+ this.onClickItem = this.onClickItem.bind(this)
64
+ }
65
+
66
+ private onCheck(value: boolean, list: Array<OptionsItem>) {
67
+ let newValue: Array<string | number> = []
68
+ if (value) {
69
+ newValue = [...this.state.value, ...list.map(e => e.id)]
70
+ } else {
71
+ newValue = this.state.value.filter(vl => list.every(e => vl !== e.id))
72
+ }
73
+ this.setState({ ...this.state, value: newValue })
74
+ if (this.props.onChange) this.props.onChange(newValue)
75
+ }
76
+
77
+ private async search(ev: React.ChangeEvent<HTMLInputElement>) {
78
+ if (ev.target.value.trim().length) {
79
+ if (this.props?.handleSearch) {
80
+ const res = await this.props.handleSearch(ev.target.value.trim())
81
+ this.setState({ ...this.state, search: res })
82
+ } else {
83
+ this.setState({
84
+ ...this.state,
85
+ search: this.props.options.filter(e => typeof e.name === "string" && e.name.toLowerCase().includes(ev.target.value.trim().toLowerCase()))
86
+ })
87
+ }
88
+ } else {
89
+ this.setState({ ...this.state, search: undefined })
90
+ }
91
+ }
92
+
93
+ private onClickItem(ev: React.MouseEvent<HTMLDivElement>, item: string | number) {
94
+ ev.stopPropagation()
95
+ let newValue = this.state.value.filter(vl => vl !== item)
96
+ this.setState({
97
+ ...this.state,
98
+ value: newValue,
99
+ ...(this.state.isOpen ? {} : {
100
+ isOpen: true,
101
+ style: undefined,
102
+ offset: this.containerRef?.current?.getBoundingClientRect() as any,
103
+ })
104
+ })
105
+ if (this.props.onChange) this.props.onChange(newValue)
106
+ }
107
+
108
+ private renderOptions(item: OptionsItem) {
109
+ let children: Array<OptionsItem> = []
110
+ if (!item.parentId) children = (this.state.search ?? this.state.options).filter(e => e.parentId === item.id)
111
+ //
112
+ return <div key={item.id} className='col' style={{ width: '100%' }}>
113
+ <div className={`${styles['select-tile']} row ${item.disabled ? styles["disabled"] : ""}`} style={{ paddingLeft: item.parentId ? '4.4rem' : undefined }} onClick={children.length ? () => {
114
+ if (this.state.search) {
115
+ this.setState({
116
+ ...this.state, search: this.state.search.map(e => {
117
+ if (e.id === item.id) return { ...e, isOpen: !(item as any).isOpen } as any
118
+ else return e
119
+ })
120
+ })
121
+ } else {
122
+ this.setState({
123
+ ...this.state, options: this.state.options.map(e => {
124
+ if (e.id === item.id) return { ...e, isOpen: !(item as any).isOpen } as any
125
+ else return e
126
+ })
127
+ })
128
+ }
129
+ } : undefined}>
130
+ {(this.state.search ?? this.state.options).some(e => e.parentId) && <div className='row' style={{ width: '1.4rem', height: '1.4rem' }}>
131
+ {children.length ? <Winicon src={(item as any).isOpen ? 'fill/arrows/triangle-down' : 'fill/arrows/triangle-right'} size={'1.2rem'} /> : null}
132
+ </div>}
133
+ <Checkbox disabled={item.disabled} value={children.length ? (children.every((e) => this.state.value.includes(e.id)) ? true : children.some((e) => this.state.value.includes(e.id)) ? undefined : false) : this.state.value.includes(item.id)} onChange={(v) => { this.onCheck(v, [item, ...children]) }} size={'2rem'} />
134
+ <Text className='body-3'>{item.name}</Text>
135
+ </div>
136
+ <div className='col' style={{ display: (item as any).isOpen ? "flex" : "none", width: '100%' }}>{children.map(e => this.renderOptions(e))}</div>
137
+ </div>
138
+ }
139
+
140
+ componentDidUpdate(prevProps: SelectMultipleProps, prevState: SelectMultipleState) {
141
+ if (prevProps.options !== this.props.options) this.setState({ ...this.state, options: this.props.options })
142
+ if (prevProps.value !== this.props.value) this.setState({ ...this.state, value: this.props.value ?? [] })
143
+ //
144
+ if (this.state.isOpen && (prevState.isOpen !== this.state.isOpen || prevState.value.length !== this.state.value.length)) {
145
+ const thisPopupRect = this.containerRef.current!.querySelector(`.select-multi-popup`)?.getBoundingClientRect()
146
+ if (thisPopupRect) {
147
+ let style: { top?: string, left?: string, right?: string, bottom?: string, width?: string, height?: string } | undefined;
148
+ if (prevState.isOpen !== this.state.isOpen && thisPopupRect.right > document.body.offsetWidth) {
149
+ style = {
150
+ top: this.state.offset.y + this.state.offset.height + 2 + 'px',
151
+ right: document.body.offsetWidth - this.state.offset.right + 'px'
152
+ }
153
+ }
154
+ let _bottom = thisPopupRect.bottom - 8
155
+ const thisContainerRect = this.containerRef.current?.getBoundingClientRect()
156
+ if (thisContainerRect) {
157
+ if (prevState.value.length !== this.state.value.length) {
158
+ _bottom = thisContainerRect.bottom + 2 + thisPopupRect.height
159
+ style = { ...(style ?? {}), top: `${thisContainerRect.bottom + 2}px` }
160
+ }
161
+ if (_bottom > document.body.offsetHeight) {
162
+ style = { ...(style ?? {}), top: `${thisContainerRect.y - 2 - thisPopupRect.height}px` }
163
+ }
164
+ }
165
+ if (style) {
166
+ style.left ??= (style.right ? undefined : `${this.state.offset.x}px`)
167
+ style.width ??= `${this.state.offset.width}px`
168
+ this.setState({ ...this.state, style: style })
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ render() {
175
+ const { t } = this.props;
176
+ return <div
177
+ id={this.props.id}
178
+ ref={this.containerRef}
179
+ className={`${styles['select-multi-container']} row ${this.props.disabled ? styles['disabled'] : ''} ${this.props.helperText?.length && styles['helper-text']} ${this.props.className ?? 'body-3'}`}
180
+ helper-text={this.props.helperText}
181
+ style={this.props.style ? { ...({ '--helper-text-color': this.props.helperTextColor ?? '#e14337' } as CSSProperties), ...this.props.style } : ({ '--helper-text-color': this.props.helperTextColor ?? '#e14337' } as CSSProperties)}
182
+ onClick={() => {
183
+ if (!this.state.isOpen) this.setState({
184
+ ...this.state,
185
+ isOpen: true,
186
+ style: undefined,
187
+ offset: this.containerRef?.current?.getBoundingClientRect() as any,
188
+ })
189
+ }}
190
+ >
191
+ <div className='row' style={{ flexWrap: 'wrap', flex: 1, width: '100%', gap: '0.6rem 0.4rem' }}>
192
+ {this.state.value.map(item => {
193
+ const optionItem = this.props.options.find(e => e.id === item)
194
+ return <div key={item} className={`row ${styles['selected-item-value']}`} onClick={optionItem?.disabled ? undefined : (ev) => this.onClickItem(ev, item)}>
195
+ <Text style={{ color: "var(--neutral-text-title-color)", fontSize: '1.2rem', lineHeight: '1.4rem' }} >{optionItem?.name}</Text>
196
+ <Winicon src={"outline/user interface/e-remove"} size={'1.2rem'} />
197
+ </div>
198
+ })}
199
+ {(!this.state.value.length || this.state.isOpen) && <input ref={this.inputRef} autoFocus={this.state.value.length > 0} onChange={this.search} placeholder={this.state.value.length ? undefined : this.props.placeholder}
200
+ onBlur={ev => {
201
+ if (this.state.isOpen) ev.target.focus()
202
+ }}
203
+ />}
204
+ </div>
205
+ {this.props.showClearValueButton && this.state.value.length ? <button type='button' className='row' style={{ padding: '0.4rem' }} onClick={(ev) => {
206
+ ev.stopPropagation()
207
+ if (this.state.value.length) this.setState({ ...this.state, isOpen: true, value: [] })
208
+ }}>
209
+ <Winicon src={"outline/user interface/c-remove"} size={'1.6rem'} />
210
+ </button> : <div ref={iconRef => {
211
+ if (iconRef?.parentElement && iconRef.parentElement.getBoundingClientRect().width < 100) iconRef.style.display = "none"
212
+ }} className='row' >
213
+ <Winicon src={this.state.isOpen ? "fill/arrows/up-arrow" : "fill/arrows/down-arrow"} size={'1.2rem'} />
214
+ </div>}
215
+ {this.state.isOpen && <PopupOverlay
216
+ className={`hidden-overlay`}
217
+ onClose={(ev) => {
218
+ if (ev.target !== this.inputRef.current) this.setState({ ...this.state, isOpen: false })
219
+ }}
220
+ >
221
+ <div className={`${styles['select-multi-popup']} select-multi-popup col ${this.props.popupClassName ?? ""}`}
222
+ style={this.state.style ?? {
223
+ top: this.state.offset.y + this.state.offset.height + 2 + 'px',
224
+ left: this.state.offset.x + 'px',
225
+ width: this.state.offset.width,
226
+ }}>
227
+ <div style={{ padding: '1.2rem 1.6rem', width: '100%', borderBottom: "var(--neutral-main-border)" }}>
228
+ {(() => {
229
+ const _list = (this.state.search ?? this.props.options ?? [])
230
+ const isSelectedAll = _list.every(item => this.state.value.some(vl => vl === item.id))
231
+ return <Text onClick={() => {
232
+ let newValue: Array<string | number> = []
233
+ if (_list.length) {
234
+ if (isSelectedAll) {
235
+ newValue = this.state.value.filter(vl => _list.every(item => vl !== item.id || item.disabled))
236
+ } else {
237
+ newValue = [...this.state.value, ..._list.filter(item => this.state.value.every(vl => vl !== item.id) && !item.disabled).map(e => e.id)]
238
+ }
239
+ }
240
+ this.setState({ ...this.state, value: newValue })
241
+ if (this.props.onChange) this.props.onChange(newValue)
242
+ }} className='button-text-3' style={{ color: _list.length ? undefined : 'var(--neutral-text-title-color)' }}>{_list.length && isSelectedAll ? `${t("remove")} ${t("all").toLowerCase()}` : `${t("select")} ${t("all").toLowerCase()}`}</Text>
243
+ })()}
244
+ </div>
245
+ <div className={`col ${styles['select-body']}`} onScroll={this.props.handleLoadmore ? (ev) => {
246
+ if (this.props.handleLoadmore) {
247
+ let scrollElement = ev.target as HTMLDivElement
248
+ this.props.handleLoadmore(Math.round(scrollElement.offsetHeight + scrollElement.scrollTop) >= (scrollElement.scrollHeight - 1), ev)
249
+ }
250
+ } : undefined}>
251
+ {(this.state.search ?? this.state.options).filter(e => !e.parentId).map(item => this.renderOptions(item))}
252
+ {(!this.state.search?.length && !this.props.options?.length) && (
253
+ <div className={styles['no-results-found']}>{t("noResultFound")}</div>
254
+ )}
255
+ </div>
256
+ </div>
257
+ </PopupOverlay>}
258
+ </div>
259
+ }
260
+ }
261
+
262
+ export const SelectMultiple = withTranslation()(TSelectMultiple)
263
+
@@ -0,0 +1,41 @@
1
+ .input-opt-container {
2
+ height: 4.8rem;
3
+ gap: 0.8rem;
4
+ }
5
+
6
+ .input-opt-container>input {
7
+ border: var(--neutral-bolder-border);
8
+ height: 100%;
9
+ aspect-ratio: 1 / 1;
10
+ border-radius: 0.8rem;
11
+ text-align: center;
12
+ min-width: 3.2rem;
13
+ font: inherit;
14
+ font-size: inherit;
15
+ font-family: inherit;
16
+ font-weight: inherit;
17
+ line-height: inherit;
18
+ font-style: inherit;
19
+ }
20
+
21
+ .input-opt-container:focus-within>input {
22
+ border-color: var(--primary-main-color);
23
+ }
24
+
25
+ .input-opt-container.helper-text {
26
+ overflow: visible !important;
27
+ border-color: var(--helper-text-color) !important;
28
+ }
29
+
30
+ .input-opt-container.helper-text::after {
31
+ content: attr(helper-text);
32
+ color: var(--helper-text-color);
33
+ position: absolute;
34
+ left: 0;
35
+ bottom: -0.4rem;
36
+ width: max-content;
37
+ font-size: 1.2rem;
38
+ line-height: 1.6rem;
39
+ font-family: inherit;
40
+ transform: translateY(100%);
41
+ }
@@ -0,0 +1,110 @@
1
+ import React, { createRef, CSSProperties, ReactNode } from "react";
2
+ import styles from './input-otp.module.css'
3
+
4
+ interface Props {
5
+ id?: string,
6
+ onChange?: (value: string, target: HTMLDivElement) => void
7
+ disabled?: boolean,
8
+ value?: string,
9
+ length?: number,
10
+ inputStyle?: CSSProperties,
11
+ style?: CSSProperties,
12
+ className?: string,
13
+ helperText?: string,
14
+ helperTextColor?: string,
15
+ autoFocus?: boolean
16
+ }
17
+
18
+ export class InputOtp extends React.Component<Props> {
19
+ private containerRef = createRef<HTMLDivElement>()
20
+ constructor(props: Props | Readonly<Props>) {
21
+ super(props);
22
+ this.getValue = this.getValue.bind(this)
23
+ }
24
+
25
+ getValue = () => {
26
+ if (this.containerRef.current)
27
+ return [...(this.containerRef.current.querySelectorAll("input") as any)].map(v => v.value).join("")
28
+ else return this.props.value ?? ""
29
+ }
30
+
31
+ componentDidUpdate(prevProps: Readonly<Props>): void {
32
+ if (prevProps.value !== this.props.value && this.containerRef.current) {
33
+ const inputList = [...(this.containerRef.current.querySelectorAll("input") as any)]
34
+ if (this.props.value?.length) {
35
+ for (let i = 0; i < inputList.length; i++) inputList[i].value = this.props.value[i]
36
+ } else {
37
+ for (let i = 0; i < inputList.length; i++) inputList[i].value = ""
38
+ }
39
+ }
40
+ }
41
+
42
+ render(): ReactNode {
43
+ return <div
44
+ id={this.props.id}
45
+ ref={this.containerRef}
46
+ helper-text={this.props.helperText}
47
+ style={this.props.style ? { ...({ '--helper-text-color': this.props.helperTextColor ?? '#e14337' } as CSSProperties), ...this.props.style } : ({ '--helper-text-color': this.props.helperTextColor ?? '#e14337' } as CSSProperties)}
48
+ className={`row body-1 ${styles['input-opt-container']} ${this.props.helperText?.length && 'helper-text'} ${this.props.className ?? ''}`}
49
+ onMouseDown={(ev: any) => {
50
+ ev.stopPropagation()
51
+ ev.preventDefault()
52
+ const inputList: any = [...ev.target.closest("div").childNodes]
53
+ for (const [index, input] of inputList.entries()) {
54
+ if (!input.value.length || index === (inputList.length - 1)) {
55
+ input.focus()
56
+ break;
57
+ }
58
+ continue;
59
+ }
60
+ }}
61
+ >
62
+ {Array.from({ length: this.props.length ?? 6 }).map((_, i) => <input
63
+ key={"opt-" + i}
64
+ autoFocus={i === 0 && this.props.autoFocus}
65
+ disabled={this.props.disabled}
66
+ style={this.props.inputStyle}
67
+ onKeyDown={(ev: any) => {
68
+ const key = ev.key.toLowerCase()
69
+ switch (key) {
70
+ case "backspace":
71
+ if (ev.target.value.length) ev.target.value = ""
72
+ else if (ev.target.previousSibling?.localName === "input") ev.target.previousSibling.focus()
73
+ else ev.target.blur()
74
+ break;
75
+ case "delete":
76
+ ev.target.value = ""
77
+ break;
78
+ default:
79
+ ev.preventDefault()
80
+ ev.stopPropagation()
81
+ if (key === "v" && ev.ctrlKey) {
82
+ return navigator.clipboard.readText().then(text => {
83
+ const otpRegex = /^\d{6}$/g
84
+ if (otpRegex.test(text)) {
85
+ const inputList: any = [...ev.target.closest("div").childNodes]
86
+ inputList.forEach((input: any, i: number) => {
87
+ input.value = text[i]
88
+ input.focus()
89
+ })
90
+ }
91
+ })
92
+ } else {
93
+ const numberCheck = /[0-9]/g
94
+ if (numberCheck.test(key) && !key.startsWith("f")) {
95
+ if (!ev.target.value.length) ev.target.value = key
96
+ if (ev.target.nextSibling?.localName === "input" && !ev.target.nextSibling.value.length) ev.target.nextSibling.focus()
97
+ else ev.target.blur()
98
+ }
99
+ }
100
+ break;
101
+ }
102
+ }}
103
+ onBlur={() => {
104
+ if (this.props.onChange) this.props.onChange(this.getValue(), this.containerRef.current!)
105
+ }}
106
+ />)}
107
+ </div>
108
+ }
109
+ }
110
+
@@ -0,0 +1,137 @@
1
+ .number-picker-container {
2
+ position: relative;
3
+ align-items: stretch;
4
+ gap: 0.4rem;
5
+ }
6
+
7
+ .number-picker-container[number-picker-type="outline"] {
8
+ border-radius: 0.8rem;
9
+ padding: 0 0.6rem;
10
+ border: var(--neutral-bolder-border);
11
+ }
12
+
13
+ .number-picker-container>div:has(>svg) {
14
+ padding: 0.7rem;
15
+ border: var(--neutral-bolder-border);
16
+ border-radius: 50%;
17
+ aspect-ratio: 1 / 1;
18
+ height: 100%;
19
+ cursor: pointer;
20
+ }
21
+
22
+ .number-picker-container[number-picker-type="outline"]>div:has(>svg) {
23
+ padding: 0.6rem;
24
+ }
25
+
26
+ .number-picker-container[class~="size40"]>div>svg {
27
+ width: 1.6rem;
28
+ height: 1.6rem;
29
+ }
30
+
31
+ .number-picker-container[class~="size40"]>div:has(>svg) {
32
+ padding: 1.1rem;
33
+ }
34
+
35
+ .number-picker-container[class~="size40"][number-picker-type="outline"]>div:has(>svg) {
36
+ padding: 1rem;
37
+ }
38
+
39
+ .number-picker-container[class~="size24"]>div:has(>svg) {
40
+ padding: 0.5rem;
41
+ }
42
+
43
+ .number-picker-container[class~="size24"][number-picker-type="outline"]>div:has(>svg) {
44
+ padding: 0.4rem;
45
+ }
46
+
47
+ .number-picker-container[class~="size24"]>div>svg {
48
+ width: 1.2rem;
49
+ height: 1.2rem;
50
+ }
51
+
52
+ .number-picker-container[number-picker-type="outline"]>div:has(>svg) {
53
+ border-color: transparent;
54
+ border-radius: 0;
55
+ }
56
+
57
+ .number-picker-container>div>svg {
58
+ height: 100%;
59
+ flex: 1;
60
+ aspect-ratio: 1 / 1;
61
+ }
62
+
63
+ .number-picker-container>div>svg * {
64
+ fill: var(--neutral-text-subtitle-color);
65
+ }
66
+
67
+ .number-picker-container>input {
68
+ flex: 1;
69
+ width: 100%;
70
+ min-width: 5.6rem;
71
+ border: var(--neutral-bolder-border);
72
+ border-color: transparent;
73
+ outline: none;
74
+ padding: 0 0.4rem;
75
+ background-color: transparent !important;
76
+ font: inherit;
77
+ color: inherit;
78
+ font-size: inherit;
79
+ font-family: inherit;
80
+ font-weight: inherit;
81
+ line-height: inherit;
82
+ text-align: center;
83
+ text-overflow: inherit;
84
+ }
85
+
86
+ .number-picker-container>input:focus {
87
+ border-color: var(--primary-main-color);
88
+ border-radius: 0.8rem;
89
+ }
90
+
91
+ .number-picker-container[number-picker-type="outline"] input {
92
+ border: none;
93
+ }
94
+
95
+ .number-picker-container[number-picker-type="outline"]:focus-within {
96
+ border-color: var(--primary-main-color);
97
+ }
98
+
99
+ .number-picker-container:has(>input:disabled) {
100
+ pointer-events: none !important;
101
+ color: var(--neutral-text-disabled-color);
102
+ }
103
+
104
+ .number-picker-container[number-picker-type="outline"]:has(>input:disabled) {
105
+ background-color: var(--neutral-disable-background-color);
106
+ }
107
+
108
+ .number-picker-container:has(>input:disabled)>div:has(>svg) {
109
+ background-color: var(--neutral-disable-background-color);
110
+ }
111
+
112
+ .number-picker-container:has(>input:disabled) svg * {
113
+ fill: var(--neutral-text-disabled-color);
114
+ }
115
+
116
+ .number-picker-container.helper-text {
117
+ overflow: visible !important;
118
+ border-color: var(--helper-text-color) !important;
119
+ }
120
+
121
+ .number-picker-container.helper-text {
122
+ overflow: visible !important;
123
+ border-color: var(--helper-text-color) !important;
124
+ }
125
+
126
+ .number-picker-container.helper-text::after {
127
+ content: attr(helper-text);
128
+ color: var(--helper-text-color);
129
+ position: absolute;
130
+ left: 0;
131
+ bottom: -0.4rem;
132
+ width: max-content;
133
+ font-size: 1.2rem;
134
+ line-height: 1.6rem;
135
+ font-family: inherit;
136
+ transform: translateY(100%);
137
+ }
@@ -0,0 +1,107 @@
1
+ import { CSSProperties, useEffect, useRef, useState } from "react";
2
+ import styles from './number-picker.module.css'
3
+
4
+ interface Props {
5
+ id?: string,
6
+ value?: number,
7
+ onChange?: (ev: number) => void,
8
+ disabled?: boolean,
9
+ readOnly?: boolean,
10
+ /**
11
+ * default: size32: body-3 \
12
+ * recommend: size40: body-2 | size24: body-3
13
+ * */
14
+ className?: string,
15
+ helperText?: string,
16
+ helperTextColor?: string,
17
+ /** default: 1 */
18
+ volume?: number,
19
+ style?: CSSProperties,
20
+ min?: number,
21
+ max?: number,
22
+ type?: "outline" | "icon-button",
23
+ }
24
+
25
+ export const NumberPicker = ({ id, value, onChange, disabled, readOnly, className, helperText, helperTextColor, max, min, style, type = "icon-button", volume = 1 }: Props) => {
26
+ const [val, setValue] = useState<number>(0);
27
+ const inputRef = useRef<HTMLInputElement>(null)
28
+
29
+ useEffect(() => {
30
+ if (inputRef.current) {
31
+ setValue(value ?? 0)
32
+ inputRef.current.value = `${value ?? 0}`
33
+ }
34
+ }, [value, inputRef])
35
+
36
+ return <div id={id}
37
+ className={`row ${styles["number-picker-container"]} ${className ?? "body-2"} ${helperText?.length && styles['helper-text']}`}
38
+ number-picker-type={type ?? "icon-button"}
39
+ helper-text={helperText}
40
+ style={style ? { ...({ '--helper-text-color': helperTextColor ?? '#e14337' } as CSSProperties), ...style } : ({ '--helper-text-color': helperTextColor ?? '#e14337' } as CSSProperties)}
41
+ >
42
+ <div className="row" onClick={() => {
43
+ let newValue = val - volume
44
+ if (min === undefined || newValue >= min) {
45
+ if (volume % 1 === 0) newValue = Math.round(newValue)
46
+ else newValue = parseFloat(newValue.toFixed(1))
47
+ setValue(newValue)
48
+ if (inputRef.current) inputRef.current.value = `${newValue}`
49
+ if (onChange) onChange(newValue)
50
+ }
51
+ }}>
52
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
53
+ <path fillRule="evenodd" clipRule="evenodd" d="M1.3335 7.93907C1.3335 7.60435 1.60484 7.33301 1.93956 7.33301H14.0608C14.3955 7.33301 14.6668 7.60435 14.6668 7.93907C14.6668 8.27379 14.3955 8.54513 14.0608 8.54513H1.93956C1.60484 8.54513 1.3335 8.27379 1.3335 7.93907Z" />
54
+ </svg>
55
+ </div>
56
+ <input
57
+ ref={inputRef}
58
+ readOnly={readOnly}
59
+ disabled={disabled}
60
+ onKeyDown={(ev) => {
61
+ switch (ev.key.toLowerCase()) {
62
+ case "enter":
63
+ (ev.target as HTMLInputElement).blur()
64
+ break;
65
+ default:
66
+ break;
67
+ }
68
+ }}
69
+ onFocus={(ev) => { (ev.target as HTMLInputElement).select() }}
70
+ onBlur={(ev) => {
71
+ let newValue = volume % 1 === 0 ? parseInt(ev.target.value.trim()) : parseFloat(ev.target.value.trim())
72
+ if (isNaN(newValue)) ev.target.value = `${val}`
73
+ else {
74
+ if (volume % 1 === 0) newValue = Math.round(newValue)
75
+ else newValue = parseFloat(newValue.toFixed(1))
76
+ if (min !== undefined && newValue < min) {
77
+ setValue(min)
78
+ if (inputRef.current) inputRef.current.value = `${min}`
79
+ if (onChange) onChange(min)
80
+ } else if (max !== undefined && newValue > max) {
81
+ setValue(max)
82
+ if (inputRef.current) inputRef.current.value = `${max}`
83
+ if (onChange) onChange(max)
84
+ } else {
85
+ setValue(newValue)
86
+ if (inputRef.current) inputRef.current.value = `${newValue}`
87
+ if (onChange) onChange(newValue)
88
+ }
89
+ }
90
+ }}
91
+ />
92
+ <div className="row" onClick={() => {
93
+ let newValue = val + volume
94
+ if (max === undefined || newValue <= max) {
95
+ if (volume % 1 === 0) newValue = Math.round(newValue)
96
+ else newValue = parseFloat(newValue.toFixed(1))
97
+ setValue(newValue)
98
+ if (inputRef.current) inputRef.current.value = `${newValue}`
99
+ if (onChange) onChange(newValue)
100
+ }
101
+ }}>
102
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
103
+ <path d="M8.60622 1.93907C8.60622 1.60435 8.33488 1.33301 8.00016 1.33301C7.66544 1.33301 7.3941 1.60435 7.3941 1.93907V7.39361H1.93956C1.60484 7.39361 1.3335 7.66496 1.3335 7.99967C1.3335 8.33439 1.60484 8.60574 1.93956 8.60574H7.3941V14.0603C7.3941 14.395 7.66544 14.6663 8.00016 14.6663C8.33488 14.6663 8.60622 14.395 8.60622 14.0603V8.60574H14.0608C14.3955 8.60574 14.6668 8.33439 14.6668 7.99967C14.6668 7.66496 14.3955 7.39361 14.0608 7.39361H8.60622V1.93907Z" />
104
+ </svg>
105
+ </div>
106
+ </div>
107
+ }