@neko-os/ui 0.0.9 → 0.0.10
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/dist/DynamicStyleTag.js +5 -0
- package/dist/DynamicStyleTag.native.js +1 -0
- package/dist/NekoUI.js +1 -1
- package/dist/abstractions/AnimatedView.web.js +1 -0
- package/dist/abstractions/FlatList.js +1 -1
- package/dist/abstractions/FlatList.native.js +1 -1
- package/dist/abstractions/StaticList.js +1 -0
- package/dist/abstractions/helpers/useSafeAreaInsets.js +1 -0
- package/dist/abstractions/helpers/useSafeAreaInsets.native.js +1 -0
- package/dist/components/actions/Dropdown.js +1 -1
- package/dist/components/actions/FloatingButton.js +1 -0
- package/dist/components/actions/index.js +1 -1
- package/dist/components/actions/menu/VerticalMenu.js +1 -1
- package/dist/components/calendar/_helpers/calendarDays.js +1 -1
- package/dist/components/feedback/alerter.js +1 -1
- package/dist/components/feedback/confirmer.js +1 -1
- package/dist/components/helpers/ConditionalLazyRender.js +1 -0
- package/dist/components/helpers/LazyAction.js +1 -0
- package/dist/components/helpers/LazyRender.js +1 -1
- package/dist/components/helpers/LazyRender.native.js +1 -1
- package/dist/components/helpers/index.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/inputs/DateInput.js +1 -1
- package/dist/components/inputs/InputWrapper.js +1 -1
- package/dist/components/inputs/LinkInput.js +1 -1
- package/dist/components/inputs/NumberInput.js +1 -0
- package/dist/components/inputs/Picker.js +1 -1
- package/dist/components/inputs/Radio.js +1 -1
- package/dist/components/inputs/RateInput.js +1 -0
- package/dist/components/inputs/SegmentedPicker.js +1 -0
- package/dist/components/inputs/Select.js +1 -0
- package/dist/components/inputs/datePicker/DayPicker.js +1 -1
- package/dist/components/inputs/datePicker/MonthPicker.js +1 -1
- package/dist/components/inputs/datePicker/QuarterPicker.js +1 -1
- package/dist/components/inputs/datePicker/WeekPicker.js +1 -1
- package/dist/components/inputs/datePicker/YearPicker.js +1 -1
- package/dist/components/inputs/index.js +1 -1
- package/dist/components/list/FlatList.js +1 -1
- package/dist/components/presentation/Rate.js +1 -0
- package/dist/components/presentation/RateTag.js +1 -0
- package/dist/components/presentation/Result.js +1 -1
- package/dist/components/presentation/Tooltip.js +1 -1
- package/dist/components/presentation/index.js +1 -1
- package/dist/components/structure/Accordion.js +1 -1
- package/dist/components/structure/Row.js +1 -1
- package/dist/components/structure/Segment.js +1 -0
- package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +1 -1
- package/dist/components/structure/bottomDrawer/native/utils.js +1 -1
- package/dist/components/structure/bottomDrawer/web/BottomDrawer.js +1 -1
- package/dist/components/structure/index.js +1 -1
- package/dist/components/structure/overlay/OverlayHandler.js +1 -1
- package/dist/components/structure/popover/Popover.js +1 -1
- package/dist/components/structure/popover/Popover_BU.js +1 -0
- package/dist/components/tabs/ActiveTabContent.js +1 -0
- package/dist/components/tabs/TabsHandler.js +1 -0
- package/dist/components/tabs/TabsMenu.js +1 -0
- package/dist/components/tabs/index.js +1 -0
- package/dist/helpers/string.js +1 -1
- package/dist/i18n/I18n.js +1 -0
- package/dist/i18n/I18nProvider.js +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/index.css +4 -0
- package/dist/index.js +1 -1
- package/dist/modifiers/animations/fadeEffect.web.js +1 -0
- package/dist/modifiers/animations/scrollEffect.web.js +1 -0
- package/dist/modifiers/animations/slideEffect.web.js +1 -0
- package/dist/modifiers/overflow.js +1 -1
- package/dist/modifiers/position.js +1 -1
- package/dist/theme/default/base.js +1 -1
- package/package.json +1 -1
- package/src/DynamicStyleTag.js +21 -0
- package/src/DynamicStyleTag.native.js +3 -0
- package/src/NekoUI.js +12 -7
- package/src/abstractions/AnimatedView.web.js +3 -0
- package/src/abstractions/FlatList.js +2 -38
- package/src/abstractions/FlatList.native.js +8 -4
- package/src/abstractions/StaticList.js +51 -0
- package/src/abstractions/helpers/useSafeAreaInsets.js +3 -0
- package/src/abstractions/helpers/useSafeAreaInsets.native.js +3 -0
- package/src/components/actions/Dropdown.js +13 -9
- package/src/components/actions/FloatingButton.js +87 -0
- package/src/components/actions/index.js +1 -0
- package/src/components/actions/menu/VerticalMenu.js +29 -4
- package/src/components/calendar/_helpers/calendarDays.js +2 -0
- package/src/components/feedback/alerter.js +1 -1
- package/src/components/feedback/confirmer.js +2 -2
- package/src/components/helpers/ConditionalLazyRender.js +6 -0
- package/src/components/helpers/LazyAction.js +22 -0
- package/src/components/helpers/LazyRender.js +2 -2
- package/src/components/helpers/LazyRender.native.js +1 -1
- package/src/components/helpers/index.js +1 -0
- package/src/components/index.js +1 -0
- package/src/components/inputs/DateInput.js +11 -1
- package/src/components/inputs/InputWrapper.js +0 -1
- package/src/components/inputs/LinkInput.js +3 -3
- package/src/components/inputs/NumberInput.js +105 -0
- package/src/components/inputs/Picker.js +61 -9
- package/src/components/inputs/Radio.js +1 -1
- package/src/components/inputs/RateInput.js +62 -0
- package/src/components/inputs/SegmentedPicker.js +62 -0
- package/src/components/inputs/Select.js +189 -0
- package/src/components/inputs/datePicker/DayPicker.js +4 -5
- package/src/components/inputs/datePicker/MonthPicker.js +2 -2
- package/src/components/inputs/datePicker/QuarterPicker.js +2 -2
- package/src/components/inputs/datePicker/WeekPicker.js +2 -2
- package/src/components/inputs/datePicker/YearPicker.js +9 -6
- package/src/components/inputs/index.js +4 -0
- package/src/components/list/FlatList.js +41 -4
- package/src/components/presentation/Rate.js +58 -0
- package/src/components/presentation/RateTag.js +35 -0
- package/src/components/presentation/Result.js +2 -2
- package/src/components/presentation/Tooltip.js +1 -0
- package/src/components/presentation/index.js +2 -0
- package/src/components/structure/Accordion.js +1 -1
- package/src/components/structure/Row.js +9 -1
- package/src/components/structure/Segment.js +51 -0
- package/src/components/structure/bottomDrawer/native/BottomDrawer.js +4 -1
- package/src/components/structure/bottomDrawer/native/utils.js +29 -22
- package/src/components/structure/bottomDrawer/web/BottomDrawer.js +3 -1
- package/src/components/structure/index.js +1 -0
- package/src/components/structure/overlay/OverlayHandler.js +6 -1
- package/src/components/structure/popover/Popover.js +33 -19
- package/src/components/structure/popover/Popover_BU.js +157 -0
- package/src/components/tabs/ActiveTabContent.js +35 -0
- package/src/components/tabs/TabsHandler.js +16 -0
- package/src/components/tabs/TabsMenu.js +15 -0
- package/src/components/tabs/index.js +3 -0
- package/src/helpers/string.js +18 -1
- package/src/i18n/I18n.js +97 -0
- package/src/i18n/I18nProvider.js +40 -0
- package/src/i18n/index.js +2 -0
- package/src/index.css +4 -0
- package/src/index.js +1 -0
- package/src/modifiers/animations/fadeEffect.web.js +3 -0
- package/src/modifiers/animations/scrollEffect.web.js +3 -0
- package/src/modifiers/animations/slideEffect.web.js +3 -0
- package/src/modifiers/overflow.js +6 -1
- package/src/modifiers/position.js +7 -0
- package/src/theme/default/base.js +6 -2
|
@@ -5,9 +5,11 @@ import tinycolor from 'tinycolor2'
|
|
|
5
5
|
import { Divider } from '../../helpers/Separator'
|
|
6
6
|
import { IconText } from '../../presentation/IconLabel'
|
|
7
7
|
import { Link } from '../Link'
|
|
8
|
+
import { List } from '../../list/FlatList'
|
|
8
9
|
import { SubmenuWrapper } from './SubmenuWrapper'
|
|
9
10
|
import { Text } from '../../text/Text'
|
|
10
11
|
import { View } from '../../structure/View'
|
|
12
|
+
import { moveScale } from '../../../theme/helpers/sizeScale'
|
|
11
13
|
import { useColorConverter } from '../../../modifiers/colorConverter'
|
|
12
14
|
import { useSizeConverter } from '../../../modifiers/sizeConverter'
|
|
13
15
|
import { useThemeComponentModifier } from '../../../modifiers/themeComponent'
|
|
@@ -15,7 +17,8 @@ import { useThemeComponentModifier } from '../../../modifiers/themeComponent'
|
|
|
15
17
|
function LinkItem({
|
|
16
18
|
item,
|
|
17
19
|
linkPaddingH = 'md',
|
|
18
|
-
linkPaddingV = '
|
|
20
|
+
linkPaddingV = 'xs',
|
|
21
|
+
linkMinHeight,
|
|
19
22
|
handlePress,
|
|
20
23
|
linkProps,
|
|
21
24
|
activeIndex,
|
|
@@ -31,6 +34,7 @@ function LinkItem({
|
|
|
31
34
|
if (!active && activeIndex >= 0) active = activeIndex === index
|
|
32
35
|
if (!active && activeKey !== undefined) active = activeKey === item.key
|
|
33
36
|
const bg = active && tinycolor(color).setAlpha(0.03).toString()
|
|
37
|
+
linkMinHeight = linkMinHeight || moveScale(sizeCode, 1)
|
|
34
38
|
|
|
35
39
|
return (
|
|
36
40
|
<SubmenuWrapper item={item} onChange={handlePress} activeKey={activeKey} color={color}>
|
|
@@ -39,9 +43,9 @@ function LinkItem({
|
|
|
39
43
|
center
|
|
40
44
|
paddingH={linkPaddingH}
|
|
41
45
|
paddingV={linkPaddingV}
|
|
46
|
+
minHeight={linkMinHeight}
|
|
42
47
|
marginR={3}
|
|
43
48
|
borderL={3}
|
|
44
|
-
marginV={-4}
|
|
45
49
|
brColor={active ? activeColor : 'transparent'}
|
|
46
50
|
bg={bg}
|
|
47
51
|
transition="border-color 0.5s ease, background 0.3s ease"
|
|
@@ -61,7 +65,7 @@ function DividerItem({ linkPaddingH = 'md', item }) {
|
|
|
61
65
|
return (
|
|
62
66
|
<>
|
|
63
67
|
{content}
|
|
64
|
-
<Text size="xs" color="text4" paddingH={linkPaddingH || 'md'} strong {...item} />
|
|
68
|
+
<Text size="xs" color="text4" paddingV="xs" paddingH={linkPaddingH || 'md'} strong {...item} />
|
|
65
69
|
</>
|
|
66
70
|
)
|
|
67
71
|
}
|
|
@@ -78,7 +82,7 @@ export function VerticalMenu(rootProps) {
|
|
|
78
82
|
useThemeComponentModifier('VerticalMenu') //
|
|
79
83
|
)([{}, rootProps])
|
|
80
84
|
|
|
81
|
-
let { gap = 'sm', items, onChange, onChangeIndex, ...props } = formattedProps
|
|
85
|
+
let { gap = 'sm', items, onChange, onChangeIndex, withDivider, ...props } = formattedProps
|
|
82
86
|
|
|
83
87
|
const handlePress = React.useCallback(
|
|
84
88
|
(item, index) => {
|
|
@@ -90,6 +94,27 @@ export function VerticalMenu(rootProps) {
|
|
|
90
94
|
[onChange, onChangeIndex]
|
|
91
95
|
)
|
|
92
96
|
|
|
97
|
+
return (
|
|
98
|
+
<View className="neko-vertical-menu" gap={gap} width="100%" {...props}>
|
|
99
|
+
<List
|
|
100
|
+
data={items}
|
|
101
|
+
keyExtractor={(item, index) => item.key || index}
|
|
102
|
+
divider={withDivider}
|
|
103
|
+
renderItem={({ item, index }) => (
|
|
104
|
+
<Item
|
|
105
|
+
key={item.key || index}
|
|
106
|
+
item={item}
|
|
107
|
+
handlePress={handlePress}
|
|
108
|
+
color={color}
|
|
109
|
+
sizeCode={sizeCode}
|
|
110
|
+
index={index}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
/>
|
|
115
|
+
</View>
|
|
116
|
+
)
|
|
117
|
+
|
|
93
118
|
return (
|
|
94
119
|
<View className="neko-vertical-menu" gap={gap} width="100%" {...props}>
|
|
95
120
|
{items.map((item, index) => (
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import dayjs from 'dayjs'
|
|
1
2
|
import React from 'react'
|
|
2
3
|
|
|
3
4
|
export function useCalendarDays(currentMonth) {
|
|
4
5
|
return React.useMemo(() => {
|
|
6
|
+
if (!currentMonth?.isValid?.()) currentMonth = dayjs()
|
|
5
7
|
const startWeekday = currentMonth.startOf('month').day()
|
|
6
8
|
const daysInMonth = currentMonth.daysInMonth()
|
|
7
9
|
|
|
@@ -16,7 +16,7 @@ export function useAlerter() {
|
|
|
16
16
|
footer: !hideClose && (
|
|
17
17
|
<Button sm label={closeLabel || 'Close'} outline color="text_op40" onPress={onClose} fullW />
|
|
18
18
|
),
|
|
19
|
-
footerProps: { borderT: false },
|
|
19
|
+
footerProps: { borderT: false, paddingV: 'md' },
|
|
20
20
|
width: width || 350,
|
|
21
21
|
}))
|
|
22
22
|
}
|
|
@@ -12,7 +12,7 @@ function Footer({ cancelLabel, confirmLabel, onConfirm, type, onClose }) {
|
|
|
12
12
|
const color = RESULT_TYPES[type]?.color || 'primary'
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<View row gap="
|
|
15
|
+
<View row gap="xs" centerV>
|
|
16
16
|
<Button sm label={cancelLabel || 'Cancel'} outline color="text_op40" onPress={onClose} flex disabled={loading} />
|
|
17
17
|
<Button
|
|
18
18
|
disabled={loading}
|
|
@@ -55,7 +55,7 @@ export function useConfirmer() {
|
|
|
55
55
|
onClose={onClose}
|
|
56
56
|
/>
|
|
57
57
|
),
|
|
58
|
-
footerProps: { borderT: false },
|
|
58
|
+
footerProps: { borderT: false, paddingV: 'md' },
|
|
59
59
|
width: width || 350,
|
|
60
60
|
}))
|
|
61
61
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { is } from 'ramda'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { LazyRender } from './LazyRender'
|
|
5
|
+
|
|
6
|
+
function InnerContent({ action }) {
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
action?.()
|
|
9
|
+
}, [])
|
|
10
|
+
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function LazyAction({ children, disabled, action, minHeight: initMinHeight, ...props }) {
|
|
15
|
+
if (!action || !is(Function, action) || !!disabled) return false
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<LazyRender whenVisible minHeight={2} {...props}>
|
|
19
|
+
<InnerContent action={action} />
|
|
20
|
+
</LazyRender>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -42,13 +42,13 @@ export function LazyRender({
|
|
|
42
42
|
}, [])
|
|
43
43
|
|
|
44
44
|
React.useEffect(() => {
|
|
45
|
-
if (ref.current
|
|
45
|
+
if (ref.current) {
|
|
46
46
|
setMinHeight(ref.current.offsetHeight)
|
|
47
47
|
}
|
|
48
48
|
}, [open])
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
|
-
<View className="neko-lazy-render" {...props} minHeight={minHeight} ref={ref}>
|
|
51
|
+
<View className="neko-lazy-render" flex="0 0 auto" {...props} minHeight={minHeight} ref={ref}>
|
|
52
52
|
{open ? children : null}
|
|
53
53
|
</View>
|
|
54
54
|
)
|
|
@@ -51,7 +51,7 @@ export function LazyRender({
|
|
|
51
51
|
}, [open])
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
|
-
<View className="neko-lazy-render" {...props} minHeight={minHeight} ref={ref}>
|
|
54
|
+
<View className="neko-lazy-render" flex="0 0 auto" {...props} minHeight={minHeight} ref={ref}>
|
|
55
55
|
{open ? children : null}
|
|
56
56
|
</View>
|
|
57
57
|
)
|
package/src/components/index.js
CHANGED
|
@@ -30,6 +30,14 @@ export function getDateInputDefaultFormat(type) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function FullWidthInputWrapper({ ref, ...props }) {
|
|
34
|
+
return (
|
|
35
|
+
<View fullW ref={ref}>
|
|
36
|
+
<MaskInput {...props} />
|
|
37
|
+
</View>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export function DateInput({
|
|
34
42
|
value,
|
|
35
43
|
onChange,
|
|
@@ -75,7 +83,7 @@ export function DateInput({
|
|
|
75
83
|
setInputValue(!!value ? dayjs(value).format(format) : '')
|
|
76
84
|
}, [value])
|
|
77
85
|
|
|
78
|
-
const Input = useBottomDrawer ? LinkInput :
|
|
86
|
+
const Input = useBottomDrawer ? LinkInput : FullWidthInputWrapper
|
|
79
87
|
|
|
80
88
|
return (
|
|
81
89
|
<Popover
|
|
@@ -83,6 +91,8 @@ export function DateInput({
|
|
|
83
91
|
placement={placement || 'bottomLeft'}
|
|
84
92
|
snapPoints={[350]}
|
|
85
93
|
useBottomDrawer={useBottomDrawer}
|
|
94
|
+
bottomDrawerProps={{ contentProps: { padding: 'md' } }}
|
|
95
|
+
watch={[value?.format?.('YYYYMMDD')]}
|
|
86
96
|
renderContent={({ onClose }) => (
|
|
87
97
|
<View flex centerH>
|
|
88
98
|
<DatePicker
|
|
@@ -4,12 +4,12 @@ import { Text } from '../text/Text'
|
|
|
4
4
|
import { View } from '../structure/View'
|
|
5
5
|
import { useColors } from '../../theme/ThemeHandler'
|
|
6
6
|
|
|
7
|
-
export function LinkInput({ onPress, onClick, placeholder, value, disabled, ...props }) {
|
|
7
|
+
export function LinkInput({ ref, onPress, onClick, placeholder, value, disabled, ...props }) {
|
|
8
8
|
return (
|
|
9
|
-
<Link onPress={!props.loading ? onPress || onClick : undefined} flex fullW centerV disabled={disabled}>
|
|
9
|
+
<Link ref={ref} onPress={!props.loading ? onPress || onClick : undefined} flex fullW centerV disabled={disabled}>
|
|
10
10
|
<InputWrapper {...props}>
|
|
11
11
|
<View centerV flex fullW>
|
|
12
|
-
<Text color={!!value ? 'text' : 'text_op30'} label={value || placeholder} />
|
|
12
|
+
<Text color={!!value ? 'text' : 'text_op30'} label={value || placeholder} numberOfLines={1} />
|
|
13
13
|
</View>
|
|
14
14
|
</InputWrapper>
|
|
15
15
|
</Link>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { endsWith, is } from 'ramda'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { TextInput } from './TextInput'
|
|
5
|
+
|
|
6
|
+
function isValidNumber(stringValue, options = {}) {
|
|
7
|
+
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER, useInt, precision } = options
|
|
8
|
+
|
|
9
|
+
if (stringValue === null || stringValue === undefined || stringValue === '') return true
|
|
10
|
+
|
|
11
|
+
if (isNaN(stringValue)) return false
|
|
12
|
+
const numericValue = parseFloat(stringValue)
|
|
13
|
+
|
|
14
|
+
if (numericValue < min) return false
|
|
15
|
+
if (numericValue > max) return false
|
|
16
|
+
|
|
17
|
+
const decimalPart = stringValue?.toString()?.split?.('.')[1]
|
|
18
|
+
|
|
19
|
+
if (decimalPart && is(Number, precision)) {
|
|
20
|
+
if (decimalPart.length > precision) return false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatNumericValue(newValue, prevValue, options = {}) {
|
|
27
|
+
const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER, useInt, precision } = options
|
|
28
|
+
let numericValue = newValue
|
|
29
|
+
|
|
30
|
+
// Handle number to string conversion
|
|
31
|
+
if (is(Number, newValue)) newValue = newValue.toString()
|
|
32
|
+
|
|
33
|
+
// Handle null/undefined/empty
|
|
34
|
+
if (newValue === null || newValue === undefined || newValue === '') return null
|
|
35
|
+
|
|
36
|
+
// Normalize decimal separator (comma to dot)
|
|
37
|
+
if (is(String, newValue)) newValue = newValue.replace(',', '.')
|
|
38
|
+
|
|
39
|
+
// Allow negative sign as intermediate state
|
|
40
|
+
if (newValue === '-') return newValue
|
|
41
|
+
|
|
42
|
+
if (useInt) {
|
|
43
|
+
// For integers, don't allow decimal points
|
|
44
|
+
if (newValue.includes('.')) return prevValue
|
|
45
|
+
numericValue = parseInt(newValue, 10)
|
|
46
|
+
} else {
|
|
47
|
+
// For floats, handle decimal points
|
|
48
|
+
const dotsCount = newValue.split('.').length
|
|
49
|
+
if (dotsCount > 2) return prevValue
|
|
50
|
+
|
|
51
|
+
// Allow "1." as intermediate state
|
|
52
|
+
if (endsWith('.', newValue)) return newValue
|
|
53
|
+
|
|
54
|
+
numericValue = parseFloat(newValue)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isNaN(numericValue)) return prevValue
|
|
58
|
+
|
|
59
|
+
// Check min/max
|
|
60
|
+
if (numericValue < min) return min
|
|
61
|
+
if (numericValue > max) return max
|
|
62
|
+
|
|
63
|
+
// Handle decimal precision
|
|
64
|
+
const decimalPart = newValue.split('.')[1]
|
|
65
|
+
if (decimalPart && !!precision) {
|
|
66
|
+
// Exceeded precision, reject
|
|
67
|
+
if (decimalPart.length > precision) return prevValue
|
|
68
|
+
// Within precision, keep as string to preserve "1.0" while typing "1.05"
|
|
69
|
+
return newValue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// No decimals, return as number
|
|
73
|
+
return numericValue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function NumberInput({ onChange, value, useInt, precision, min, max, error, ...props }) {
|
|
77
|
+
const [hasError, setHasError] = React.useState(false)
|
|
78
|
+
const [inputValue, setInputValue] = React.useState(value)
|
|
79
|
+
const [localValue, setLocalValue] = React.useState(value)
|
|
80
|
+
React.useEffect(() => setInputValue(value), [value])
|
|
81
|
+
|
|
82
|
+
if (useInt) precision = 0
|
|
83
|
+
if (!useInt && precision === 0) useInt = true
|
|
84
|
+
const opts = { useInt, precision, min, max }
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<TextInput
|
|
88
|
+
onChange={(newValue) => {
|
|
89
|
+
const numericValue = formatNumericValue(newValue, localValue, opts)
|
|
90
|
+
setInputValue(newValue?.toString() || '')
|
|
91
|
+
setLocalValue(numericValue)
|
|
92
|
+
onChange?.(numericValue)
|
|
93
|
+
setHasError(!isValidNumber(newValue, opts))
|
|
94
|
+
}}
|
|
95
|
+
onBlur={() => {
|
|
96
|
+
setInputValue(localValue)
|
|
97
|
+
setHasError(!isValidNumber(localValue, opts))
|
|
98
|
+
}}
|
|
99
|
+
value={inputValue}
|
|
100
|
+
keyboardType={useInt ? 'number-pad' : 'decimal-pad'}
|
|
101
|
+
error={error || hasError}
|
|
102
|
+
{...props}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -1,10 +1,35 @@
|
|
|
1
|
+
import { is } from 'ramda'
|
|
1
2
|
import React from 'react'
|
|
2
3
|
|
|
3
4
|
import { Col } from '../structure/Col'
|
|
5
|
+
import { FlatList } from '../list/FlatList'
|
|
4
6
|
import { LoadingView } from '../state/LoadingView'
|
|
5
7
|
import { Row } from '../structure/Row'
|
|
8
|
+
import { normalizeString } from '../../helpers/string'
|
|
6
9
|
import { useOptions } from '../../helpers/options'
|
|
7
10
|
|
|
11
|
+
export function getOption(options, value, config = {}) {
|
|
12
|
+
if (!options?.length) return value
|
|
13
|
+
const option = options.find((option) => compareOptionsValues(option, value, config))
|
|
14
|
+
return option || value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getOptionLabel(options, value, config = {}) {
|
|
18
|
+
if (!options?.length) return ''
|
|
19
|
+
const { labelKey } = config
|
|
20
|
+
const selectedOption = getOption(options, value, config)
|
|
21
|
+
const label = selectedOption?.[labelKey] || value
|
|
22
|
+
if (!is(String, label)) return ''
|
|
23
|
+
return label
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function searchOptions(options, search, config = {}) {
|
|
27
|
+
const { labelKey } = config
|
|
28
|
+
if (!options?.length) return options
|
|
29
|
+
if (!search) return options
|
|
30
|
+
return options.filter((item) => normalizeString(item?.[labelKey])?.includes(normalizeString(search)))
|
|
31
|
+
}
|
|
32
|
+
|
|
8
33
|
function isSelected(value, option, config = {}) {
|
|
9
34
|
return !!config.multiple
|
|
10
35
|
? value?.some?.((item) => compareOptionsValues(item, option, config))
|
|
@@ -53,12 +78,33 @@ function PickerItem({ option, onChange, value, renderOption, useRawOption, multi
|
|
|
53
78
|
const handleChange = () => {
|
|
54
79
|
const formatChangeValueFunc = multiple ? formatMultipleChangeValue : formatSingleChangeValue
|
|
55
80
|
const formattedValue = formatChangeValueFunc(option, value, { selected, useRawOption, valueKey })
|
|
56
|
-
onChange(formattedValue)
|
|
81
|
+
onChange(formattedValue, option)
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
return <Col {...props}>{renderOption({ option, selected, onChange: handleChange, valueKey, labelKey })}</Col>
|
|
60
85
|
}
|
|
61
86
|
|
|
87
|
+
function DefaultPickerWrapper({ renderItem, options, ...props }) {
|
|
88
|
+
return (
|
|
89
|
+
<Row className="neko-picker" gap="md" {...props}>
|
|
90
|
+
{options?.map?.((option) => renderItem(option))}
|
|
91
|
+
</Row>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function FlatListPickerWrapper({ renderItem, options, valueKey, ...props }) {
|
|
96
|
+
return (
|
|
97
|
+
<FlatList
|
|
98
|
+
keyExtractor={(i) => i[valueKey]}
|
|
99
|
+
data={options}
|
|
100
|
+
divider
|
|
101
|
+
fullH
|
|
102
|
+
renderItem={({ item: option }) => renderItem(option)}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
62
108
|
export function Picker({
|
|
63
109
|
value,
|
|
64
110
|
initialValue,
|
|
@@ -68,23 +114,24 @@ export function Picker({
|
|
|
68
114
|
renderOption,
|
|
69
115
|
colProps,
|
|
70
116
|
useRawOption,
|
|
117
|
+
useFlatList,
|
|
71
118
|
multiple,
|
|
72
119
|
valueKey,
|
|
73
120
|
labelKey,
|
|
121
|
+
Wrapper,
|
|
74
122
|
...rootProps
|
|
75
123
|
}) {
|
|
76
124
|
const [localValue, setLocalValue] = React.useState(initialValue)
|
|
77
125
|
value = value === undefined ? localValue : value
|
|
78
126
|
onChange = onChange || setLocalValue
|
|
127
|
+
const { options: finalOptions, isFirstLoad } = useOptions(options, {})
|
|
79
128
|
|
|
80
|
-
const handleChange = (v) => {
|
|
129
|
+
const handleChange = (v, option) => {
|
|
81
130
|
if (!!disabled) return
|
|
82
131
|
setLocalValue(v)
|
|
83
|
-
onChange?.(v)
|
|
132
|
+
onChange?.(v, option)
|
|
84
133
|
}
|
|
85
134
|
|
|
86
|
-
const { options: finalOptions, isFirstLoad } = useOptions(options, {})
|
|
87
|
-
|
|
88
135
|
valueKey = valueKey || 'value'
|
|
89
136
|
labelKey = labelKey || 'label'
|
|
90
137
|
|
|
@@ -93,10 +140,15 @@ export function Picker({
|
|
|
93
140
|
return false
|
|
94
141
|
}
|
|
95
142
|
|
|
143
|
+
Wrapper = Wrapper || (useFlatList ? FlatListPickerWrapper : DefaultPickerWrapper)
|
|
144
|
+
|
|
96
145
|
return (
|
|
97
146
|
<LoadingView active={isFirstLoad} replaceChildren>
|
|
98
|
-
<
|
|
99
|
-
{
|
|
147
|
+
<Wrapper
|
|
148
|
+
{...rootProps}
|
|
149
|
+
valueKey={valueKey}
|
|
150
|
+
options={finalOptions}
|
|
151
|
+
renderItem={(option) => (
|
|
100
152
|
<PickerItem
|
|
101
153
|
key={option.value}
|
|
102
154
|
option={option}
|
|
@@ -109,8 +161,8 @@ export function Picker({
|
|
|
109
161
|
labelKey={labelKey}
|
|
110
162
|
{...colProps}
|
|
111
163
|
/>
|
|
112
|
-
)
|
|
113
|
-
|
|
164
|
+
)}
|
|
165
|
+
/>
|
|
114
166
|
</LoadingView>
|
|
115
167
|
)
|
|
116
168
|
}
|
|
@@ -33,7 +33,7 @@ export function Radio({ value, onChange, disabled, initialValue, ...rootProps })
|
|
|
33
33
|
size={sizeCode}
|
|
34
34
|
gap={8}
|
|
35
35
|
content={
|
|
36
|
-
<View height={size * 0.65} ratio={1} border={2} padding={
|
|
36
|
+
<View height={size * 0.65} ratio={1} border={2} padding={2} borderColor={color} br={size} center>
|
|
37
37
|
{!!value && <View bg={color} br={size} flex fullW fullH />}
|
|
38
38
|
</View>
|
|
39
39
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { pipe, range, is } from 'ramda'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { Icon } from '../presentation'
|
|
5
|
+
import { Link } from '../actions/Link'
|
|
6
|
+
import { LoadingView } from '../state'
|
|
7
|
+
import { View } from '../structure/View'
|
|
8
|
+
import { moveScale } from '../../theme/helpers/sizeScale'
|
|
9
|
+
import { useColorConverter } from '../../modifiers/colorConverter'
|
|
10
|
+
import { useDefaultModifier } from '../../modifiers/default'
|
|
11
|
+
import { useSizeConverter } from '../../modifiers/sizeConverter'
|
|
12
|
+
import { useThemeComponentModifier } from '../../modifiers/themeComponent'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PROPS = {
|
|
15
|
+
color: 'primary',
|
|
16
|
+
inactiveColor: 'text4_op50',
|
|
17
|
+
max: 5,
|
|
18
|
+
icon: 'star-fill',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function RateInput({ value, onChange, disabled, loading, ...rootProps }) {
|
|
22
|
+
let [{ size, sizeCode, color }, formattedProps] = pipe(
|
|
23
|
+
useColorConverter('primary'),
|
|
24
|
+
useSizeConverter('icons', 'md'),
|
|
25
|
+
useThemeComponentModifier('RateInput'),
|
|
26
|
+
useDefaultModifier(DEFAULT_PROPS)
|
|
27
|
+
)([{}, rootProps])
|
|
28
|
+
|
|
29
|
+
const [localValue, setLocalValue] = React.useState(value)
|
|
30
|
+
React.useEffect(() => setLocalValue(value), [value])
|
|
31
|
+
|
|
32
|
+
const { icon, max, inactiveColor, ...props } = formattedProps
|
|
33
|
+
|
|
34
|
+
const handleChange = (v) => {
|
|
35
|
+
if (!!disabled) return
|
|
36
|
+
const newValue = v === localValue ? null : v
|
|
37
|
+
setLocalValue(newValue)
|
|
38
|
+
onChange?.(newValue)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<LoadingView active={loading} width="fit-content">
|
|
43
|
+
<View className="neko-rate-input" row gap="xs" centerV minHeight={sizeCode} {...props}>
|
|
44
|
+
{range(1, max + 1).map((i) => {
|
|
45
|
+
const active = localValue >= i
|
|
46
|
+
|
|
47
|
+
let finalIcon = icon
|
|
48
|
+
if (is(Function, icon)) finalIcon = icon?.({ value: localValue, optionValue: i, active })
|
|
49
|
+
|
|
50
|
+
let finalColor = color
|
|
51
|
+
if (is(Function, color)) finalColor = color?.({ value: localValue, optionValue: i, active })
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Link onPress={() => handleChange(i)} disabled={disabled} center key={i}>
|
|
55
|
+
<Icon name={finalIcon} size={moveScale(sizeCode, 1)} color={active ? finalColor : inactiveColor} />
|
|
56
|
+
</Link>
|
|
57
|
+
)
|
|
58
|
+
})}
|
|
59
|
+
</View>
|
|
60
|
+
</LoadingView>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { pipe } from 'ramda'
|
|
2
|
+
|
|
3
|
+
import { Button } from '../actions'
|
|
4
|
+
import { Picker } from './Picker'
|
|
5
|
+
import { View } from '../structure'
|
|
6
|
+
import { moveScale } from '../../theme/helpers/sizeScale'
|
|
7
|
+
import { useDefaultModifier } from '../../modifiers/default'
|
|
8
|
+
import { useSizeConverter } from '../../modifiers/sizeConverter'
|
|
9
|
+
import { useThemeComponentModifier } from '../../modifiers/themeComponent'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PROPS = ([{ sizeCode }]) => ({
|
|
12
|
+
gap: 1,
|
|
13
|
+
br: sizeCode,
|
|
14
|
+
minHeight: sizeCode,
|
|
15
|
+
bg: 'overlayBG',
|
|
16
|
+
padding: 2,
|
|
17
|
+
wrap: false,
|
|
18
|
+
row: true,
|
|
19
|
+
border: true,
|
|
20
|
+
buttonProps: {
|
|
21
|
+
fullH: true,
|
|
22
|
+
size: sizeCode,
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function PickerWrapper({ renderItem, options, ...props }) {
|
|
27
|
+
return (
|
|
28
|
+
<View row>
|
|
29
|
+
<View {...props}>{options?.map?.((option) => renderItem(option))}</View>
|
|
30
|
+
</View>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function SegmentedPicker({ ...rootProps }) {
|
|
35
|
+
const [{ sizeCode }, formattedProps] = pipe(
|
|
36
|
+
useSizeConverter('elementHeights', 'md'),
|
|
37
|
+
useThemeComponentModifier('SegmentPicker'),
|
|
38
|
+
useDefaultModifier(DEFAULT_PROPS)
|
|
39
|
+
)([{}, rootProps])
|
|
40
|
+
|
|
41
|
+
const { buttonProps, color, ...props } = formattedProps
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Picker
|
|
45
|
+
className="neko-segmented-picker"
|
|
46
|
+
Wrapper={PickerWrapper}
|
|
47
|
+
{...props}
|
|
48
|
+
renderOption={({ option, selected, onChange, labelKey, ...props }) => (
|
|
49
|
+
<Button
|
|
50
|
+
label={option[labelKey]}
|
|
51
|
+
onPress={onChange}
|
|
52
|
+
color={selected ? color || 'primary' : rootProps?.bg || 'overlayBG'}
|
|
53
|
+
round={rootProps?.round}
|
|
54
|
+
textProps={{ strong: selected }}
|
|
55
|
+
opacity={!selected && 0.8}
|
|
56
|
+
{...option}
|
|
57
|
+
{...buttonProps}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|