@neko-os/ui 0.0.9 → 0.0.11

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 (143) hide show
  1. package/dist/DynamicStyleTag.js +5 -0
  2. package/dist/DynamicStyleTag.native.js +1 -0
  3. package/dist/NekoUI.js +1 -1
  4. package/dist/abstractions/AnimatedView.web.js +1 -0
  5. package/dist/abstractions/FlatList.js +1 -1
  6. package/dist/abstractions/FlatList.native.js +1 -1
  7. package/dist/abstractions/StaticList.js +1 -0
  8. package/dist/abstractions/helpers/useSafeAreaInsets.js +1 -0
  9. package/dist/abstractions/helpers/useSafeAreaInsets.native.js +1 -0
  10. package/dist/components/actions/Button.js +1 -1
  11. package/dist/components/actions/Dropdown.js +1 -1
  12. package/dist/components/actions/FloatingButton.js +1 -0
  13. package/dist/components/actions/index.js +1 -1
  14. package/dist/components/actions/menu/VerticalMenu.js +1 -1
  15. package/dist/components/calendar/_helpers/calendarDays.js +1 -1
  16. package/dist/components/feedback/alerter.js +1 -1
  17. package/dist/components/feedback/confirmer.js +1 -1
  18. package/dist/components/helpers/ConditionalLazyRender.js +1 -0
  19. package/dist/components/helpers/LazyAction.js +1 -0
  20. package/dist/components/helpers/LazyRender.js +1 -1
  21. package/dist/components/helpers/LazyRender.native.js +1 -1
  22. package/dist/components/helpers/index.js +1 -1
  23. package/dist/components/index.js +1 -1
  24. package/dist/components/inputs/DateInput.js +1 -1
  25. package/dist/components/inputs/InputWrapper.js +1 -1
  26. package/dist/components/inputs/LinkInput.js +1 -1
  27. package/dist/components/inputs/NumberInput.js +1 -0
  28. package/dist/components/inputs/Picker.js +1 -1
  29. package/dist/components/inputs/Radio.js +1 -1
  30. package/dist/components/inputs/RateInput.js +1 -0
  31. package/dist/components/inputs/SegmentedPicker.js +1 -0
  32. package/dist/components/inputs/Select.js +1 -0
  33. package/dist/components/inputs/datePicker/DayPicker.js +1 -1
  34. package/dist/components/inputs/datePicker/MonthPicker.js +1 -1
  35. package/dist/components/inputs/datePicker/QuarterPicker.js +1 -1
  36. package/dist/components/inputs/datePicker/WeekPicker.js +1 -1
  37. package/dist/components/inputs/datePicker/YearPicker.js +1 -1
  38. package/dist/components/inputs/index.js +1 -1
  39. package/dist/components/list/FlatList.js +1 -1
  40. package/dist/components/presentation/Rate.js +1 -0
  41. package/dist/components/presentation/RateTag.js +1 -0
  42. package/dist/components/presentation/Result.js +1 -1
  43. package/dist/components/presentation/Tooltip.js +1 -1
  44. package/dist/components/presentation/index.js +1 -1
  45. package/dist/components/structure/Accordion.js +1 -1
  46. package/dist/components/structure/Row.js +1 -1
  47. package/dist/components/structure/Segment.js +1 -0
  48. package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +1 -1
  49. package/dist/components/structure/bottomDrawer/native/utils.js +1 -1
  50. package/dist/components/structure/bottomDrawer/web/BottomDrawer.js +1 -1
  51. package/dist/components/structure/index.js +1 -1
  52. package/dist/components/structure/overlay/OverlayHandler.js +1 -1
  53. package/dist/components/structure/popover/Popover.js +1 -1
  54. package/dist/components/structure/popover/Popover_BU.js +1 -0
  55. package/dist/components/tabs/ActiveTabContent.js +1 -0
  56. package/dist/components/tabs/TabsHandler.js +1 -0
  57. package/dist/components/tabs/TabsMenu.js +1 -0
  58. package/dist/components/tabs/index.js +1 -0
  59. package/dist/helpers/string.js +1 -1
  60. package/dist/i18n/I18n.js +1 -0
  61. package/dist/i18n/I18nProvider.js +1 -0
  62. package/dist/i18n/index.js +1 -0
  63. package/dist/index.css +4 -0
  64. package/dist/index.js +1 -1
  65. package/dist/modifiers/animations/fadeEffect.web.js +1 -0
  66. package/dist/modifiers/animations/scrollEffect.web.js +1 -0
  67. package/dist/modifiers/animations/slideEffect.web.js +1 -0
  68. package/dist/modifiers/fullColor.js +1 -1
  69. package/dist/modifiers/overflow.js +1 -1
  70. package/dist/modifiers/position.js +1 -1
  71. package/dist/theme/default/base.js +1 -1
  72. package/package.json +1 -1
  73. package/src/DynamicStyleTag.js +21 -0
  74. package/src/DynamicStyleTag.native.js +3 -0
  75. package/src/NekoUI.js +12 -7
  76. package/src/abstractions/AnimatedView.web.js +3 -0
  77. package/src/abstractions/FlatList.js +2 -38
  78. package/src/abstractions/FlatList.native.js +8 -4
  79. package/src/abstractions/StaticList.js +51 -0
  80. package/src/abstractions/helpers/useSafeAreaInsets.js +3 -0
  81. package/src/abstractions/helpers/useSafeAreaInsets.native.js +3 -0
  82. package/src/components/actions/Button.js +15 -13
  83. package/src/components/actions/Dropdown.js +13 -9
  84. package/src/components/actions/FloatingButton.js +87 -0
  85. package/src/components/actions/index.js +1 -0
  86. package/src/components/actions/menu/VerticalMenu.js +29 -4
  87. package/src/components/calendar/_helpers/calendarDays.js +2 -0
  88. package/src/components/feedback/alerter.js +1 -1
  89. package/src/components/feedback/confirmer.js +2 -2
  90. package/src/components/helpers/ConditionalLazyRender.js +6 -0
  91. package/src/components/helpers/LazyAction.js +22 -0
  92. package/src/components/helpers/LazyRender.js +2 -2
  93. package/src/components/helpers/LazyRender.native.js +1 -1
  94. package/src/components/helpers/index.js +1 -0
  95. package/src/components/index.js +1 -0
  96. package/src/components/inputs/DateInput.js +11 -1
  97. package/src/components/inputs/InputWrapper.js +0 -1
  98. package/src/components/inputs/LinkInput.js +3 -3
  99. package/src/components/inputs/NumberInput.js +105 -0
  100. package/src/components/inputs/Picker.js +61 -9
  101. package/src/components/inputs/Radio.js +1 -1
  102. package/src/components/inputs/RateInput.js +62 -0
  103. package/src/components/inputs/SegmentedPicker.js +62 -0
  104. package/src/components/inputs/Select.js +189 -0
  105. package/src/components/inputs/datePicker/DayPicker.js +4 -5
  106. package/src/components/inputs/datePicker/MonthPicker.js +2 -2
  107. package/src/components/inputs/datePicker/QuarterPicker.js +2 -2
  108. package/src/components/inputs/datePicker/WeekPicker.js +2 -2
  109. package/src/components/inputs/datePicker/YearPicker.js +9 -6
  110. package/src/components/inputs/index.js +4 -0
  111. package/src/components/list/FlatList.js +41 -4
  112. package/src/components/presentation/Rate.js +58 -0
  113. package/src/components/presentation/RateTag.js +35 -0
  114. package/src/components/presentation/Result.js +2 -2
  115. package/src/components/presentation/Tooltip.js +1 -0
  116. package/src/components/presentation/index.js +2 -0
  117. package/src/components/structure/Accordion.js +1 -1
  118. package/src/components/structure/Row.js +9 -1
  119. package/src/components/structure/Segment.js +51 -0
  120. package/src/components/structure/bottomDrawer/native/BottomDrawer.js +4 -1
  121. package/src/components/structure/bottomDrawer/native/utils.js +29 -22
  122. package/src/components/structure/bottomDrawer/web/BottomDrawer.js +3 -1
  123. package/src/components/structure/index.js +1 -0
  124. package/src/components/structure/overlay/OverlayHandler.js +6 -1
  125. package/src/components/structure/popover/Popover.js +33 -19
  126. package/src/components/structure/popover/Popover_BU.js +157 -0
  127. package/src/components/tabs/ActiveTabContent.js +35 -0
  128. package/src/components/tabs/TabsHandler.js +16 -0
  129. package/src/components/tabs/TabsMenu.js +15 -0
  130. package/src/components/tabs/index.js +3 -0
  131. package/src/helpers/string.js +18 -1
  132. package/src/i18n/I18n.js +97 -0
  133. package/src/i18n/I18nProvider.js +40 -0
  134. package/src/i18n/index.js +2 -0
  135. package/src/index.css +4 -0
  136. package/src/index.js +1 -0
  137. package/src/modifiers/animations/fadeEffect.web.js +3 -0
  138. package/src/modifiers/animations/scrollEffect.web.js +3 -0
  139. package/src/modifiers/animations/slideEffect.web.js +3 -0
  140. package/src/modifiers/fullColor.js +5 -2
  141. package/src/modifiers/overflow.js +6 -1
  142. package/src/modifiers/position.js +7 -0
  143. package/src/theme/default/base.js +6 -2
@@ -0,0 +1,87 @@
1
+ import { pipe } from 'ramda'
2
+
3
+ import { AbsTouchableOpacity } from '../../abstractions/TouchableOpacity'
4
+ import { Icon } from '../presentation'
5
+ import { Loading } from '../state'
6
+ import { moveScale } from '../../theme/helpers/sizeScale'
7
+ import { useBackgroundModifier } from '../../modifiers/background'
8
+ import { useBorderModifier } from '../../modifiers/border'
9
+ import { useColorConverter } from '../../modifiers/colorConverter'
10
+ import { useCursorModifier } from '../../modifiers/cursor'
11
+ import { useDefaultModifier } from '../../modifiers/default'
12
+ import { useDisplayModifier } from '../../modifiers/display'
13
+ import { useFlexModifier } from '../../modifiers/flex'
14
+ import { useFlexWrapperModifier } from '../../modifiers/flexWrapper'
15
+ import { useFullColorModifier } from '../../modifiers/fullColor'
16
+ import { useHoverConverter } from '../../modifiers/hover'
17
+ import { useMarginModifier } from '../../modifiers/margin'
18
+ import { usePaddingModifier } from '../../modifiers/padding'
19
+ import { usePositionModifier } from '../../modifiers/position'
20
+ import { useSafeAreaInsets } from '../../abstractions/helpers/useSafeAreaInsets'
21
+ import { useSizeConverter } from '../../modifiers/sizeConverter'
22
+ import { useSizeModifier } from '../../modifiers/size'
23
+ import { useStateModifier } from '../../modifiers/state'
24
+ import { useThemeComponentModifier } from '../../modifiers/themeComponent'
25
+
26
+ const DEFAULT_PROPS =
27
+ ({ bottomInset }) =>
28
+ ([{ sizeCode }, { square }]) => {
29
+ sizeCode = moveScale(sizeCode, 1)
30
+
31
+ return {
32
+ absolute: true,
33
+ shadow: true,
34
+ bottom: 'md',
35
+ marginB: bottomInset,
36
+ right: 'md',
37
+ round: !square,
38
+ ration: 1,
39
+ padding: 'xxxs',
40
+ height: sizeCode,
41
+ width: sizeCode,
42
+ br: sizeCode,
43
+ border: 1,
44
+ center: true,
45
+ pointer: true,
46
+ hover: {
47
+ opacity: 0.7,
48
+ },
49
+ }
50
+ }
51
+
52
+ export function FloatingButton({ useSafeArea = true, ...rootProps }) {
53
+ const insets = useSafeAreaInsets()
54
+ const bottomInset = useSafeArea ? insets.bottom : 0
55
+
56
+ let [{ loading, fontColor, color, sizeCode }, formattedProps] = pipe(
57
+ useColorConverter('primary'),
58
+ useSizeConverter('elementHeights', 'md'),
59
+ useThemeComponentModifier('FloatingButton'),
60
+ useDefaultModifier(DEFAULT_PROPS({ bottomInset })),
61
+ useHoverConverter,
62
+ useCursorModifier,
63
+ useFullColorModifier,
64
+ useDisplayModifier,
65
+ useStateModifier,
66
+ useSizeModifier,
67
+ usePositionModifier,
68
+ usePaddingModifier,
69
+ useMarginModifier,
70
+ useFlexModifier,
71
+ useFlexWrapperModifier,
72
+ useBackgroundModifier,
73
+ useBorderModifier
74
+ )([{}, rootProps])
75
+ sizeCode = moveScale(sizeCode, 1)
76
+
77
+ const { icon, iconProps, size, ...props } = formattedProps
78
+
79
+ let content = <Icon flex color={fontColor} size={sizeCode} name={icon} loading={loading} {...iconProps} />
80
+ if (loading) content = <Loading size={sizeCode} color={fontColor} />
81
+
82
+ return (
83
+ <AbsTouchableOpacity className="neko-floating-button neko-wave-click-effect" type="button" {...props}>
84
+ {content}
85
+ </AbsTouchableOpacity>
86
+ )
87
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './Button'
2
+ export * from './FloatingButton'
2
3
  export * from './Link'
3
4
  export * from './Pressable'
4
5
  export * from './Dropdown'
@@ -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 = 'sm',
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="sm" centerV>
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,6 @@
1
+ import { LazyRender } from './LazyRender'
2
+
3
+ export function ConditionalLazyRender({ children, ...props }) {
4
+ if (!props.delay && !props.whenVisible && !props.destroyOffScreen) return children
5
+ return <LazyRender {...props}>{children}</LazyRender>
6
+ }
@@ -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 && open) {
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
  )
@@ -4,3 +4,4 @@ export * from './VerticalView'
4
4
  export * from './PortalHandler'
5
5
  export * from './Portal'
6
6
  export * from './LazyRender'
7
+ export * from './ConditionalLazyRender'
@@ -12,4 +12,5 @@ export * from './table'
12
12
  export * from './feedback'
13
13
  export * from './calendar'
14
14
  export * from './list'
15
+ export * from './tabs'
15
16
  export * from './theme'
@@ -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 : MaskInput
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
@@ -10,7 +10,6 @@ import { useSizeConverter } from '../../modifiers/sizeConverter'
10
10
  import { useThemeComponentModifier } from '../../modifiers/themeComponent'
11
11
 
12
12
  const DEFAULT_PROPS = {
13
- fullW: '100%', //
14
13
  paddingH: 'sm',
15
14
  bg: 'overlayBG',
16
15
  border: true,
@@ -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
- <Row className="neko-picker" gap="md" {...rootProps}>
99
- {finalOptions?.map?.((option) => (
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
- </Row>
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={3} borderColor={color} br={size} center>
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
+ }