@neko-os/ui 0.0.8 → 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.
Files changed (220) 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/ActivityIndicator.native.js +1 -1
  5. package/dist/abstractions/ActivityIndicator.web.js +1 -0
  6. package/dist/abstractions/AnimatedView.web.js +1 -0
  7. package/dist/abstractions/BlurView.web.js +1 -0
  8. package/dist/abstractions/FlatList.js +1 -0
  9. package/dist/abstractions/FlatList.native.js +1 -0
  10. package/dist/abstractions/FlatList.web.js +1 -0
  11. package/dist/abstractions/ScrollView.web.js +1 -0
  12. package/dist/abstractions/StaticList.js +1 -0
  13. package/dist/abstractions/helpers/storage.js +1 -0
  14. package/dist/abstractions/helpers/storage.native.js +1 -0
  15. package/dist/abstractions/helpers/useSafeAreaInsets.js +1 -0
  16. package/dist/abstractions/helpers/useSafeAreaInsets.native.js +1 -0
  17. package/dist/components/actions/Button.js +1 -1
  18. package/dist/components/actions/Dropdown.js +1 -1
  19. package/dist/components/actions/FloatingButton.js +1 -0
  20. package/dist/components/actions/index.js +1 -1
  21. package/dist/components/actions/menu/VerticalMenu.js +1 -1
  22. package/dist/components/calendar/_helpers/calendarDays.js +1 -1
  23. package/dist/components/feedback/alerter.js +1 -1
  24. package/dist/components/feedback/confirmer.js +1 -1
  25. package/dist/components/helpers/ConditionalLazyRender.js +1 -0
  26. package/dist/components/helpers/LazyAction.js +1 -0
  27. package/dist/components/helpers/LazyRender.js +1 -1
  28. package/dist/components/helpers/LazyRender.native.js +1 -1
  29. package/dist/components/helpers/index.js +1 -1
  30. package/dist/components/index.js +1 -1
  31. package/dist/components/inputs/DateInput.js +1 -1
  32. package/dist/components/inputs/InputWrapper.js +1 -1
  33. package/dist/components/inputs/LinkInput.js +1 -1
  34. package/dist/components/inputs/NumberInput.js +1 -0
  35. package/dist/components/inputs/Picker.js +1 -1
  36. package/dist/components/inputs/Radio.js +1 -1
  37. package/dist/components/inputs/RateInput.js +1 -0
  38. package/dist/components/inputs/SegmentedPicker.js +1 -0
  39. package/dist/components/inputs/Select.js +1 -0
  40. package/dist/components/inputs/datePicker/DayPicker.js +1 -1
  41. package/dist/components/inputs/datePicker/MonthPicker.js +1 -1
  42. package/dist/components/inputs/datePicker/QuarterPicker.js +1 -1
  43. package/dist/components/inputs/datePicker/WeekPicker.js +1 -1
  44. package/dist/components/inputs/datePicker/YearPicker.js +1 -1
  45. package/dist/components/inputs/index.js +1 -1
  46. package/dist/components/layout/Layout.js +1 -1
  47. package/dist/components/list/FlatList.js +1 -0
  48. package/dist/components/list/index.js +1 -1
  49. package/dist/components/presentation/Rate.js +1 -0
  50. package/dist/components/presentation/RateTag.js +1 -0
  51. package/dist/components/presentation/Result.js +1 -1
  52. package/dist/components/presentation/Tooltip.js +1 -1
  53. package/dist/components/presentation/index.js +1 -1
  54. package/dist/components/state/LoadingView.js +1 -1
  55. package/dist/components/structure/Accordion.js +1 -1
  56. package/dist/components/structure/Row.js +1 -1
  57. package/dist/components/structure/Segment.js +1 -0
  58. package/dist/components/structure/View.js +1 -1
  59. package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +1 -1
  60. package/dist/components/structure/bottomDrawer/native/utils.js +1 -1
  61. package/dist/components/structure/bottomDrawer/web/BottomDrawer.js +1 -1
  62. package/dist/components/structure/index.js +1 -1
  63. package/dist/components/structure/overlay/OverlayHandler.js +1 -1
  64. package/dist/components/structure/popover/Popover.js +1 -1
  65. package/dist/components/structure/popover/Popover.native.js +1 -1
  66. package/dist/components/structure/popover/Popover_BU.js +1 -0
  67. package/dist/components/tabs/ActiveTabContent.js +1 -0
  68. package/dist/components/tabs/TabsHandler.js +1 -0
  69. package/dist/components/tabs/TabsMenu.js +1 -0
  70. package/dist/components/tabs/index.js +1 -0
  71. package/dist/components/theme/ThemePicker.js +1 -0
  72. package/dist/components/theme/ThemePickerDrawer.js +1 -0
  73. package/dist/components/theme/ThemeStatusBar.js +1 -0
  74. package/dist/components/theme/ThemeStatusBar.native.js +1 -0
  75. package/dist/components/theme/ThemeThumb.js +1 -0
  76. package/dist/components/theme/index.js +1 -0
  77. package/dist/helpers/index.js +1 -1
  78. package/dist/helpers/storage.js +1 -0
  79. package/dist/helpers/string.js +1 -1
  80. package/dist/i18n/I18n.js +1 -0
  81. package/dist/i18n/I18nProvider.js +1 -0
  82. package/dist/i18n/index.js +1 -0
  83. package/dist/index.css +4 -0
  84. package/dist/index.js +1 -1
  85. package/dist/modifiers/animations/fadeEffect.web.js +1 -0
  86. package/dist/modifiers/animations/scrollEffect.web.js +1 -0
  87. package/dist/modifiers/animations/slideEffect.web.js +1 -0
  88. package/dist/modifiers/fullColor.js +1 -1
  89. package/dist/modifiers/overflow.js +1 -1
  90. package/dist/modifiers/position.js +1 -1
  91. package/dist/theme/ThemeHandler.js +1 -1
  92. package/dist/theme/default/base.js +1 -1
  93. package/dist/theme/default/blackTheme.js +1 -1
  94. package/dist/theme/default/cyberpunkTheme.js +1 -1
  95. package/dist/theme/default/darkTheme.js +1 -1
  96. package/dist/theme/default/deepWoodsTheme.js +1 -1
  97. package/dist/theme/default/forestTheme.js +1 -1
  98. package/dist/theme/default/hackerTheme.js +1 -1
  99. package/dist/theme/default/lightTheme.js +1 -1
  100. package/dist/theme/default/midnightTheme.js +1 -1
  101. package/dist/theme/default/msdosTheme.js +1 -1
  102. package/dist/theme/default/oceanTheme.js +1 -1
  103. package/dist/theme/default/paperTheme.js +1 -0
  104. package/dist/theme/default/pastelTheme.js +1 -1
  105. package/dist/theme/default/sunsetTheme.js +1 -1
  106. package/dist/theme/default/themes.js +1 -1
  107. package/dist/theme/format/formatTheme.js +1 -1
  108. package/dist/theme/helpers/contrastColor.js +1 -1
  109. package/package.json +1 -1
  110. package/src/DynamicStyleTag.js +21 -0
  111. package/src/DynamicStyleTag.native.js +3 -0
  112. package/src/NekoUI.js +21 -4
  113. package/src/abstractions/ActivityIndicator.native.js +3 -4
  114. package/src/abstractions/ActivityIndicator.web.js +43 -0
  115. package/src/abstractions/AnimatedView.web.js +3 -0
  116. package/src/abstractions/BlurView.web.js +39 -0
  117. package/src/abstractions/FlatList.js +3 -0
  118. package/src/abstractions/FlatList.native.js +36 -0
  119. package/src/abstractions/FlatList.web.js +3 -0
  120. package/src/abstractions/ScrollView.web.js +3 -0
  121. package/src/abstractions/StaticList.js +51 -0
  122. package/src/abstractions/Text.web.js +15 -0
  123. package/src/abstractions/helpers/storage.js +32 -0
  124. package/src/abstractions/helpers/storage.native.js +34 -0
  125. package/src/abstractions/helpers/useSafeAreaInsets.js +3 -0
  126. package/src/abstractions/helpers/useSafeAreaInsets.native.js +3 -0
  127. package/src/components/actions/Button.js +1 -0
  128. package/src/components/actions/Dropdown.js +24 -5
  129. package/src/components/actions/FloatingButton.js +87 -0
  130. package/src/components/actions/index.js +1 -0
  131. package/src/components/actions/menu/VerticalMenu.js +30 -5
  132. package/src/components/calendar/_helpers/calendarDays.js +2 -0
  133. package/src/components/feedback/alerter.js +1 -1
  134. package/src/components/feedback/confirmer.js +2 -2
  135. package/src/components/helpers/ConditionalLazyRender.js +6 -0
  136. package/src/components/helpers/LazyAction.js +22 -0
  137. package/src/components/helpers/LazyRender.js +2 -2
  138. package/src/components/helpers/LazyRender.native.js +1 -1
  139. package/src/components/helpers/index.js +1 -0
  140. package/src/components/index.js +2 -0
  141. package/src/components/inputs/DateInput.js +11 -1
  142. package/src/components/inputs/InputWrapper.js +0 -1
  143. package/src/components/inputs/LinkInput.js +3 -3
  144. package/src/components/inputs/NumberInput.js +105 -0
  145. package/src/components/inputs/Picker.js +61 -9
  146. package/src/components/inputs/Radio.js +1 -1
  147. package/src/components/inputs/RateInput.js +62 -0
  148. package/src/components/inputs/SegmentedPicker.js +62 -0
  149. package/src/components/inputs/Select.js +189 -0
  150. package/src/components/inputs/datePicker/DayPicker.js +4 -5
  151. package/src/components/inputs/datePicker/MonthPicker.js +2 -2
  152. package/src/components/inputs/datePicker/QuarterPicker.js +2 -2
  153. package/src/components/inputs/datePicker/WeekPicker.js +2 -2
  154. package/src/components/inputs/datePicker/YearPicker.js +9 -6
  155. package/src/components/inputs/index.js +4 -0
  156. package/src/components/layout/Layout.js +1 -1
  157. package/src/components/list/FlatList.js +91 -0
  158. package/src/components/list/index.js +1 -0
  159. package/src/components/presentation/Rate.js +58 -0
  160. package/src/components/presentation/RateTag.js +35 -0
  161. package/src/components/presentation/Result.js +2 -2
  162. package/src/components/presentation/Tooltip.js +1 -0
  163. package/src/components/presentation/index.js +2 -0
  164. package/src/components/state/LoadingView.js +10 -1
  165. package/src/components/structure/Accordion.js +1 -1
  166. package/src/components/structure/Row.js +9 -1
  167. package/src/components/structure/Segment.js +51 -0
  168. package/src/components/structure/View.js +2 -0
  169. package/src/components/structure/bottomDrawer/native/BottomDrawer.js +19 -3
  170. package/src/components/structure/bottomDrawer/native/utils.js +29 -22
  171. package/src/components/structure/bottomDrawer/web/BottomDrawer.js +3 -1
  172. package/src/components/structure/index.js +1 -0
  173. package/src/components/structure/overlay/OverlayHandler.js +6 -1
  174. package/src/components/structure/popover/Popover.js +44 -21
  175. package/src/components/structure/popover/Popover.native.js +3 -2
  176. package/src/components/structure/popover/Popover_BU.js +157 -0
  177. package/src/components/tabs/ActiveTabContent.js +35 -0
  178. package/src/components/tabs/TabsHandler.js +16 -0
  179. package/src/components/tabs/TabsMenu.js +15 -0
  180. package/src/components/tabs/index.js +3 -0
  181. package/src/components/theme/ThemePicker.js +49 -0
  182. package/src/components/theme/ThemePickerDrawer.js +13 -0
  183. package/src/components/theme/ThemeStatusBar.js +3 -0
  184. package/src/components/theme/ThemeStatusBar.native.js +9 -0
  185. package/src/components/theme/ThemeThumb.js +98 -0
  186. package/src/components/theme/index.js +3 -0
  187. package/src/helpers/index.js +1 -0
  188. package/src/helpers/storage.js +54 -0
  189. package/src/helpers/string.js +18 -1
  190. package/src/i18n/I18n.js +97 -0
  191. package/src/i18n/I18nProvider.js +40 -0
  192. package/src/i18n/index.js +2 -0
  193. package/src/index.css +4 -0
  194. package/src/index.js +1 -0
  195. package/src/modifiers/animations/fadeEffect.web.js +3 -0
  196. package/src/modifiers/animations/scrollEffect.web.js +3 -0
  197. package/src/modifiers/animations/slideEffect.web.js +3 -0
  198. package/src/modifiers/fullColor.js +2 -2
  199. package/src/modifiers/overflow.js +6 -1
  200. package/src/modifiers/position.js +7 -0
  201. package/src/theme/ThemeHandler.js +18 -2
  202. package/src/theme/default/base.js +12 -8
  203. package/src/theme/default/blackTheme.js +4 -1
  204. package/src/theme/default/cyberpunkTheme.js +3 -1
  205. package/src/theme/default/darkTheme.js +3 -1
  206. package/src/theme/default/deepWoodsTheme.js +4 -2
  207. package/src/theme/default/forestTheme.js +3 -1
  208. package/src/theme/default/hackerTheme.js +3 -1
  209. package/src/theme/default/lightTheme.js +3 -1
  210. package/src/theme/default/midnightTheme.js +3 -1
  211. package/src/theme/default/msdosTheme.js +18 -4
  212. package/src/theme/default/oceanTheme.js +4 -2
  213. package/src/theme/default/paperTheme.js +35 -0
  214. package/src/theme/default/pastelTheme.js +3 -1
  215. package/src/theme/default/sunsetTheme.js +5 -3
  216. package/src/theme/default/themes.js +7 -10
  217. package/src/theme/format/formatTheme.js +9 -3
  218. package/src/theme/helpers/contrastColor.js +49 -11
  219. package/dist/abstractions/TouchableOpacity.web.js +0 -1
  220. package/src/abstractions/TouchableOpacity.web.js +0 -3
@@ -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,3 +12,5 @@ export * from './table'
12
12
  export * from './feedback'
13
13
  export * from './calendar'
14
14
  export * from './list'
15
+ export * from './tabs'
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
+ }
@@ -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
+ }
@@ -0,0 +1,189 @@
1
+ import React from 'react'
2
+
3
+ import { Icon, IconLabel } from '../presentation'
4
+ import { Link } from '../actions'
5
+ import { LinkInput } from './LinkInput'
6
+ import { Picker, getOptionLabel, searchOptions } from './Picker'
7
+ import { Popover } from '../structure/popover/Popover'
8
+ import { TextInput } from './TextInput'
9
+ import { View } from '../structure'
10
+ import { useResponsiveValue } from '../../responsive'
11
+
12
+ function FullWidthInputWrapper({ ref, ...props }) {
13
+ return (
14
+ <View fullW ref={ref}>
15
+ <TextInput {...props} />
16
+ </View>
17
+ )
18
+ }
19
+
20
+ export function Select({
21
+ value,
22
+ onChange,
23
+ onChangeSearch,
24
+ options,
25
+ placement,
26
+ placeholder,
27
+ initialLabel,
28
+ useBottomDrawer = { native: true, sm: true, md: true },
29
+ useSearch,
30
+ renderOption,
31
+ labelKey,
32
+ valueKey,
33
+ useRawOption,
34
+ multiple,
35
+ onEndReached,
36
+ renderFooter,
37
+ renderHeader,
38
+ pickerProps,
39
+ popoverProps,
40
+ popoverMaxHeight,
41
+ ...props
42
+ }) {
43
+ const [focus, setFocus] = React.useState(false)
44
+ const [search, setSearch] = React.useState('')
45
+ const [inputValue, setInputValue] = React.useState(initialLabel || '')
46
+ const [localValue, setLocalValue] = React.useState(value)
47
+
48
+ labelKey = labelKey || pickerProps?.labelKey || 'label'
49
+ valueKey = valueKey || pickerProps?.valueKey || 'value'
50
+ useRawOption = useRawOption || pickerProps?.useRawOption
51
+ multiple = multiple || pickerProps?.multiple
52
+ onEndReached = onEndReached || pickerProps?.onEndReached
53
+ renderFooter = renderFooter || pickerProps?.renderFooter
54
+ renderHeader = renderHeader || pickerProps?.renderHeader
55
+ pickerProps = { ...pickerProps, labelKey, valueKey, useRawOption, multiple, onEndReached, renderFooter, renderHeader }
56
+
57
+ popoverMaxHeight = popoverMaxHeight || 300
58
+
59
+ useBottomDrawer = useResponsiveValue(useBottomDrawer)
60
+
61
+ value = value || localValue
62
+
63
+ const handleChange = React.useCallback(
64
+ (value, option) => {
65
+ if (!!multiple) {
66
+ setInputValue(value.map((item) => getOptionLabel(options, item, { valueKey, labelKey })).join(', '))
67
+ } else {
68
+ setInputValue(option?.[labelKey] || getOptionLabel(options, option, { valueKey, labelKey }))
69
+ }
70
+ setLocalValue(value)
71
+ onChange?.(value)
72
+ },
73
+ [labelKey, valueKey]
74
+ )
75
+
76
+ React.useEffect(() => {
77
+ if (!!multiple) {
78
+ setInputValue(value?.map?.((item) => getOptionLabel(options, item, { valueKey, labelKey })).join(', '))
79
+ } else {
80
+ const label = value?.[labelKey] || getOptionLabel(options, value, { valueKey, labelKey })
81
+ if (!!label || !value) setInputValue(label)
82
+ }
83
+ }, [value])
84
+
85
+ const handleChangeSearch = React.useCallback((v) => {
86
+ onChangeSearch?.(v)
87
+ setSearch(v)
88
+ }, [])
89
+
90
+ const Input = !useSearch || useBottomDrawer ? LinkInput : FullWidthInputWrapper
91
+ const valueWatcher = multiple && localValue
92
+
93
+ const finalRenderOption = React.useCallback(
94
+ (params) => {
95
+ if (!!renderOption) return renderOption(params)
96
+ const { option, labelKey, selected } = params
97
+ return <IconLabel {...option} label={option?.[labelKey]} flex strong={selected} />
98
+ },
99
+ [renderOption]
100
+ )
101
+
102
+ return (
103
+ <Popover
104
+ trigger="click"
105
+ placement={placement || 'bottomLeft'}
106
+ snapPoints={[450]}
107
+ useBottomDrawer={useBottomDrawer}
108
+ parentWidth
109
+ padding={0}
110
+ watch={[search, options, valueWatcher]}
111
+ unmountOnClose
112
+ maxHeight={popoverMaxHeight}
113
+ {...popoverProps}
114
+ renderContent={({ onClose }) => (
115
+ <>
116
+ {useBottomDrawer && useSearch && (
117
+ <View padding="md" paddingB="xs">
118
+ <TextInput
119
+ prefixIcon="search-line"
120
+ prefixIconColor="text4"
121
+ value={search}
122
+ onChange={handleChangeSearch}
123
+ />
124
+ </View>
125
+ )}
126
+ <Picker
127
+ row={false}
128
+ options={searchOptions(options, search, { labelKey })}
129
+ value={value}
130
+ gap={0}
131
+ maxHeight={!useBottomDrawer && popoverMaxHeight}
132
+ useFlatList
133
+ onlyOnScreen
134
+ itemMinHeight={30}
135
+ onChange={(v, option) => {
136
+ handleChange(v, option)
137
+ if (!multiple) onClose()
138
+ }}
139
+ {...pickerProps}
140
+ renderOption={({ option, selected, onChange }) => (
141
+ <Link
142
+ row
143
+ paddingH={useBottomDrawer ? 'md' : 'sm'}
144
+ paddingV="xs"
145
+ minHeight={useBottomDrawer ? 'xl' : 'md'}
146
+ gap="sm"
147
+ onMouseDown={(e) => !!multiple && e.preventDefault()}
148
+ onPress={onChange}
149
+ centerV
150
+ bg={selected && 'primary_op10'}
151
+ >
152
+ <View flex row>
153
+ {finalRenderOption({ option, labelKey, selected })}
154
+ </View>
155
+ {selected && <Icon name="checkbox-circle-fill" primary />}
156
+ </Link>
157
+ )}
158
+ />
159
+ </>
160
+ )}
161
+ >
162
+ <Input
163
+ value={!!focus ? search : inputValue}
164
+ onChange={handleChangeSearch}
165
+ onFocus={() => {
166
+ handleChangeSearch('')
167
+ setFocus(true)
168
+ }}
169
+ onBlur={() => {
170
+ setTimeout(() => {
171
+ setFocus(false)
172
+ }, 200)
173
+ }}
174
+ // When the option to use tags presentation, use prefix to render the tags
175
+ // _prefix={
176
+ // multiple &&
177
+ // value?.length && (
178
+ // <Text>{value?.map((item) => getOptionLabel(options, item, { labelKey, valueKey })).join(', ')}</Text>
179
+ // )
180
+ // }
181
+ placeholder={(!multiple || !value?.length) && placeholder}
182
+ suffixIcon="arrow-down-s-fill"
183
+ suffixIconColor="text4"
184
+ fullW
185
+ {...props}
186
+ />
187
+ </Popover>
188
+ )
189
+ }