@neko-os/ui 0.4.0 → 0.5.1

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 (77) hide show
  1. package/dist/abstractions/KeyboardDismissView.js +3 -0
  2. package/dist/abstractions/KeyboardDismissView.native.js +9 -0
  3. package/dist/abstractions/index.js +1 -0
  4. package/dist/components/actions/ClearLink.js +6 -0
  5. package/dist/components/actions/FloatingMenu.js +1 -1
  6. package/dist/components/calendar/CalendarNav.js +6 -6
  7. package/dist/components/carousel/Carousel.js +48 -0
  8. package/dist/components/carousel/CarouselArrows.js +40 -0
  9. package/dist/components/carousel/CarouselArrows.native.js +40 -0
  10. package/dist/components/carousel/CarouselDots.js +32 -0
  11. package/dist/components/carousel/CarouselDots.native.js +36 -0
  12. package/dist/components/carousel/CarouselHandler.js +86 -0
  13. package/dist/components/carousel/CarouselSlider.js +124 -0
  14. package/dist/components/carousel/CarouselSlider.native.js +121 -0
  15. package/dist/components/carousel/InfiniteCarousel.js +50 -0
  16. package/dist/components/carousel/index.js +6 -0
  17. package/dist/components/form/Form.js +5 -3
  18. package/dist/components/index.js +3 -1
  19. package/dist/components/inputs/DateInput.js +7 -4
  20. package/dist/components/inputs/InputWrapper.js +1 -2
  21. package/dist/components/inputs/Select.js +56 -52
  22. package/dist/components/inputs/TextInput.js +7 -6
  23. package/dist/components/inputs/datePicker/DayPicker.js +71 -23
  24. package/dist/components/inputs/datePicker/MonthPicker.js +61 -32
  25. package/dist/components/inputs/datePicker/QuarterPicker.js +62 -33
  26. package/dist/components/inputs/datePicker/WeekPicker.js +65 -24
  27. package/dist/components/inputs/datePicker/YearPicker.js +69 -40
  28. package/dist/components/keyboard/KeyboardDismissButton.js +3 -0
  29. package/dist/components/keyboard/KeyboardDismissButton.native.js +38 -0
  30. package/dist/components/keyboard/index.js +1 -0
  31. package/dist/components/modals/bottomDrawer/native/BottomDrawer.js +28 -7
  32. package/dist/components/presentation/LabelValue.js +1 -1
  33. package/dist/components/presentation/Result.js +11 -3
  34. package/dist/components/structure/KeyboardAvoidingView.js +9 -2
  35. package/dist/components/theme/ThemePicker.js +7 -12
  36. package/dist/components/theme/ThemeThumb.js +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/theme/ThemeHandler.js +31 -3
  39. package/package.json +1 -1
  40. package/src/abstractions/KeyboardDismissView.js +3 -0
  41. package/src/abstractions/KeyboardDismissView.native.js +9 -0
  42. package/src/abstractions/index.js +1 -0
  43. package/src/components/actions/ClearLink.js +6 -0
  44. package/src/components/actions/FloatingMenu.js +1 -1
  45. package/src/components/calendar/CalendarNav.js +6 -6
  46. package/src/components/carousel/Carousel.js +48 -0
  47. package/src/components/carousel/CarouselArrows.js +40 -0
  48. package/src/components/carousel/CarouselArrows.native.js +40 -0
  49. package/src/components/carousel/CarouselDots.js +32 -0
  50. package/src/components/carousel/CarouselDots.native.js +36 -0
  51. package/src/components/carousel/CarouselHandler.js +86 -0
  52. package/src/components/carousel/CarouselSlider.js +124 -0
  53. package/src/components/carousel/CarouselSlider.native.js +121 -0
  54. package/src/components/carousel/InfiniteCarousel.js +50 -0
  55. package/src/components/carousel/index.js +6 -0
  56. package/src/components/form/Form.js +2 -0
  57. package/src/components/index.js +2 -0
  58. package/src/components/inputs/DateInput.js +4 -1
  59. package/src/components/inputs/InputWrapper.js +1 -2
  60. package/src/components/inputs/Select.js +19 -15
  61. package/src/components/inputs/TextInput.js +5 -4
  62. package/src/components/inputs/datePicker/DayPicker.js +69 -21
  63. package/src/components/inputs/datePicker/MonthPicker.js +60 -31
  64. package/src/components/inputs/datePicker/QuarterPicker.js +60 -31
  65. package/src/components/inputs/datePicker/WeekPicker.js +63 -22
  66. package/src/components/inputs/datePicker/YearPicker.js +68 -39
  67. package/src/components/keyboard/KeyboardDismissButton.js +3 -0
  68. package/src/components/keyboard/KeyboardDismissButton.native.js +38 -0
  69. package/src/components/keyboard/index.js +1 -0
  70. package/src/components/modals/bottomDrawer/native/BottomDrawer.js +27 -6
  71. package/src/components/presentation/LabelValue.js +1 -1
  72. package/src/components/presentation/Result.js +10 -2
  73. package/src/components/structure/KeyboardAvoidingView.js +9 -2
  74. package/src/components/theme/ThemePicker.js +8 -13
  75. package/src/components/theme/ThemeThumb.js +1 -1
  76. package/src/index.js +1 -0
  77. package/src/theme/ThemeHandler.js +31 -3
@@ -0,0 +1,121 @@
1
+ import React from 'react'
2
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler'
3
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS } from 'react-native-reanimated'
4
+
5
+ import { View } from '../structure/View'
6
+ import { useCarousel } from './CarouselHandler'
7
+
8
+ function SlideContent({ item }) {
9
+ const Content = React.useMemo(
10
+ () => item.render || item.renderContent || item.Content,
11
+ [item.render, item.renderContent, item.Content]
12
+ )
13
+ return Content ? <Content /> : null
14
+ }
15
+
16
+ function clampIndex(index, count, loop) {
17
+ 'worklet'
18
+ if (loop) return ((index % count) + count) % count
19
+ return Math.max(0, Math.min(index, count - 1))
20
+ }
21
+
22
+ export function CarouselSlider() {
23
+ const { items, activeIndex, itemsCount, draggable, goTo, afterChange, pauseAutoplay, resumeAutoplay, loop } =
24
+ useCarousel()
25
+
26
+ const [slideWidth, setSlideWidth] = React.useState(0)
27
+ const translateX = useSharedValue(0)
28
+ const gestureStartX = useSharedValue(0)
29
+ const prevItemsRef = React.useRef(items)
30
+ const gestureAnimatingRef = React.useRef(false)
31
+
32
+ const setGestureAnimating = React.useCallback((v) => {
33
+ gestureAnimatingRef.current = v
34
+ }, [])
35
+
36
+ React.useEffect(() => {
37
+ if (slideWidth > 0) {
38
+ if (prevItemsRef.current !== items) {
39
+ prevItemsRef.current = items
40
+ translateX.value = -activeIndex * slideWidth
41
+ } else if (gestureAnimatingRef.current) {
42
+ // Gesture onEnd already animating — skip to avoid double animation
43
+ gestureAnimatingRef.current = false
44
+ } else {
45
+ translateX.value = withTiming(-activeIndex * slideWidth, { duration: 300 }, (finished) => {
46
+ if (finished && afterChange) runOnJS(afterChange)(items?.[activeIndex]?.key, activeIndex)
47
+ })
48
+ }
49
+ }
50
+ }, [activeIndex, slideWidth, items])
51
+
52
+ const onLayout = React.useCallback((e) => {
53
+ setSlideWidth(e.nativeEvent.layout.width)
54
+ }, [])
55
+
56
+ const animatedStyle = useAnimatedStyle(() => ({
57
+ transform: [{ translateX: translateX.value }],
58
+ }))
59
+
60
+ if (!items?.length) return null
61
+
62
+ const minTranslate = -(itemsCount - 1) * slideWidth
63
+ const maxTranslate = 0
64
+
65
+ const panGesture = Gesture.Pan()
66
+ .activeOffsetX([-10, 10])
67
+ .failOffsetY([-10, 10])
68
+ .onStart(() => {
69
+ gestureStartX.value = translateX.value
70
+ runOnJS(pauseAutoplay)()
71
+ })
72
+ .onUpdate((e) => {
73
+ const raw = gestureStartX.value + e.translationX
74
+ if (loop) {
75
+ translateX.value = raw
76
+ } else {
77
+ // Rubber band resistance at edges
78
+ if (raw > maxTranslate) {
79
+ translateX.value = maxTranslate + (raw - maxTranslate) * 0.3
80
+ } else if (raw < minTranslate) {
81
+ translateX.value = minTranslate + (raw - minTranslate) * 0.3
82
+ } else {
83
+ translateX.value = raw
84
+ }
85
+ }
86
+ })
87
+ .onEnd((e) => {
88
+ const threshold = slideWidth * 0.25
89
+ let targetIndex = activeIndex
90
+
91
+ if (e.translationX < -threshold || e.velocityX < -500) {
92
+ targetIndex = activeIndex + 1
93
+ } else if (e.translationX > threshold || e.velocityX > 500) {
94
+ targetIndex = activeIndex - 1
95
+ }
96
+
97
+ const clamped = clampIndex(targetIndex, itemsCount, loop)
98
+ translateX.value = withTiming(-clamped * slideWidth, { duration: 300 }, (finished) => {
99
+ if (finished && afterChange) runOnJS(afterChange)(items?.[clamped]?.key, clamped)
100
+ })
101
+ runOnJS(setGestureAnimating)(true)
102
+ runOnJS(goTo)(targetIndex)
103
+ runOnJS(resumeAutoplay)()
104
+ })
105
+
106
+ const track = (
107
+ <Animated.View style={[{ flexDirection: 'row', width: slideWidth * itemsCount }, animatedStyle]}>
108
+ {items.map((item) => (
109
+ <View key={item.key} style={{ width: slideWidth }}>
110
+ <SlideContent item={item} />
111
+ </View>
112
+ ))}
113
+ </Animated.View>
114
+ )
115
+
116
+ return (
117
+ <View hiddenOverflow fullW onLayout={onLayout}>
118
+ {draggable ? <GestureDetector gesture={panGesture}>{track}</GestureDetector> : track}
119
+ </View>
120
+ )
121
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react'
2
+
3
+ import { Carousel } from './Carousel'
4
+
5
+ function buildItems(value, min, max, renderSlideRef) {
6
+ const items = []
7
+ if (min === undefined || value - 1 >= min) {
8
+ items.push({ key: value - 1, render: () => renderSlideRef.current(value - 1) })
9
+ }
10
+ items.push({ key: value, render: () => renderSlideRef.current(value) })
11
+ if (max === undefined || value + 1 <= max) {
12
+ items.push({ key: value + 1, render: () => renderSlideRef.current(value + 1) })
13
+ }
14
+ return items
15
+ }
16
+
17
+ export function InfiniteCarousel({ value, onChange, renderSlide, min, max, ...carouselProps }) {
18
+ const renderSlideRef = React.useRef(renderSlide)
19
+ renderSlideRef.current = renderSlide
20
+
21
+ const [items, setItems] = React.useState(() => buildItems(value, min, max, renderSlideRef))
22
+ const [activeKey, setActiveKey] = React.useState(value)
23
+
24
+ React.useEffect(() => {
25
+ setItems(buildItems(value, min, max, renderSlideRef))
26
+ setActiveKey(value)
27
+ }, [value, min, max])
28
+
29
+ const handleChange = React.useCallback((key) => {
30
+ setActiveKey(key)
31
+ }, [])
32
+
33
+ const handleAfterChange = React.useCallback(
34
+ (key) => {
35
+ if (key !== value) onChange?.(key)
36
+ },
37
+ [value, onChange]
38
+ )
39
+
40
+ return (
41
+ <Carousel
42
+ items={items}
43
+ activeKey={activeKey}
44
+ onChange={handleChange}
45
+ afterChange={handleAfterChange}
46
+ draggable
47
+ {...carouselProps}
48
+ />
49
+ )
50
+ }
@@ -0,0 +1,6 @@
1
+ export * from './CarouselHandler'
2
+ export * from './Carousel'
3
+ export * from './CarouselSlider'
4
+ export * from './CarouselDots'
5
+ export * from './CarouselArrows'
6
+ export * from './InfiniteCarousel'
@@ -1,6 +1,7 @@
1
1
  import React from 'react'
2
2
 
3
3
  import { FormWrapperComponent } from './FormWrapperComponent'
4
+ import { KeyboardDismissButton } from '../keyboard/KeyboardDismissButton'
4
5
  import { LoadingView } from '../state/LoadingView'
5
6
  import { useNewForm } from './useNewForm'
6
7
 
@@ -24,6 +25,7 @@ export function Form({ form, onSubmit, onValuesChange, initialValues, children,
24
25
  <LoadingView active={loading} noWrapper>
25
26
  <FormWrapperComponent form={form} onSubmit={onSubmit} gap="md" {...props}>
26
27
  {children}
28
+ <KeyboardDismissButton />
27
29
  </FormWrapperComponent>
28
30
  </LoadingView>
29
31
  </FormContext.Provider>
@@ -7,6 +7,7 @@ export * from './modals'
7
7
  export * from './text'
8
8
  export * from './helpers'
9
9
  export * from './inputs'
10
+ export * from './keyboard'
10
11
  export * from './state'
11
12
  export * from './layout'
12
13
  export * from './table'
@@ -18,3 +19,4 @@ export * from './theme'
18
19
  export * from './sections'
19
20
  export * from './filter'
20
21
  export * from './steps'
22
+ export * from './carousel'
@@ -48,6 +48,7 @@ export function DateInput({
48
48
  type = 'day',
49
49
  format,
50
50
  startsOpen,
51
+ allowClear,
51
52
  useBottomDrawer = { native: true, sm: true, md: true },
52
53
  ...props
53
54
  }) {
@@ -91,7 +92,7 @@ export function DateInput({
91
92
  trigger="click"
92
93
  startsOpen={startsOpen}
93
94
  placement={placement || 'bottomLeft'}
94
- snapPoints={[350]}
95
+ snapPoints={[450]}
95
96
  useBottomDrawer={useBottomDrawer}
96
97
  bottomDrawerProps={{ contentProps: { padding: 'md' } }}
97
98
  watch={[value?.format?.('YYYYMMDD')]}
@@ -103,6 +104,8 @@ export function DateInput({
103
104
  handleChange(v)
104
105
  onClose()
105
106
  }}
107
+ width={useBottomDrawer ? '100%' : 275}
108
+ allowClear={allowClear}
106
109
  {...validations}
107
110
  type={type}
108
111
  />
@@ -65,8 +65,7 @@ export function InputWrapper({
65
65
  <View
66
66
  className="neko-input-wrapper"
67
67
  height={multiline ? undefined : size}
68
- minHeight={multiline ? size : undefined}
69
- paddingV={multiline ? 'sm' : undefined}
68
+ minHeight={multiline ? 1.5 * size : undefined}
70
69
  onPress={handlePress}
71
70
  borderColor={borderColor}
72
71
  onMouseEnter={() => setHover(true)}
@@ -2,6 +2,7 @@ import { dissoc } from 'ramda'
2
2
  import React from 'react'
3
3
 
4
4
  import { Icon, IconLabel } from '../presentation'
5
+ import { KeyboardDismissButton } from '../keyboard'
5
6
  import { Link } from '../actions'
6
7
  import { LinkInput } from './LinkInput'
7
8
  import { Picker, getOptionLabel, searchOptions } from './Picker'
@@ -134,6 +135,8 @@ export function Select({
134
135
  maxHeight={popoverMaxHeight}
135
136
  {...popoverProps}
136
137
  renderContent={({ onClose }) => (
138
+ <>
139
+ {useBottomDrawer && useSearch && <KeyboardDismissButton />}
137
140
  <Picker
138
141
  row={false}
139
142
  options={searchOptions(options, search, { labelKey })}
@@ -152,21 +155,21 @@ export function Select({
152
155
  }}
153
156
  {...pickerProps}
154
157
  renderHeader={
155
- useBottomDrawer && useSearch
156
- ? () => (
157
- <>
158
- <View padding="md" paddingB="xs">
159
- <TextInput
160
- prefixIcon="search-line"
161
- prefixIconColor="text4"
162
- value={search}
163
- onChange={handleChangeSearch}
164
- />
165
- </View>
166
- {renderHeader?.()}
167
- </>
168
- )
169
- : renderHeader
158
+ useBottomDrawer && useSearch ? (
159
+ <>
160
+ <View padding="md" paddingB="xs">
161
+ <TextInput
162
+ prefixIcon="search-line"
163
+ prefixIconColor="text4"
164
+ value={search}
165
+ onChange={handleChangeSearch}
166
+ />
167
+ </View>
168
+ {renderHeader?.()}
169
+ </>
170
+ ) : (
171
+ renderHeader
172
+ )
170
173
  }
171
174
  renderOption={({ option, selected, onChange }) => (
172
175
  <Link
@@ -187,6 +190,7 @@ export function Select({
187
190
  </Link>
188
191
  )}
189
192
  />
193
+ </>
190
194
  )}
191
195
  >
192
196
  <Input
@@ -1,9 +1,11 @@
1
1
  import { AbsTextInput } from '../../abstractions/TextInput'
2
2
  import { InputWrapper } from './InputWrapper'
3
3
  import { useColors } from '../../theme/ThemeHandler'
4
+ import { useSpaces } from '../../theme'
4
5
 
5
6
  export function TextInput({ onChange, multiline, rows, ...props }) {
6
7
  const colors = useColors()
8
+ const spaces = useSpaces()
7
9
 
8
10
  const STYLE = {
9
11
  width: '100%',
@@ -11,10 +13,9 @@ export function TextInput({ onChange, multiline, rows, ...props }) {
11
13
  background: 'transparent',
12
14
  outline: 'none',
13
15
  color: colors.text,
14
- // fontFamily: 'inherit',
15
- // fontSize: 'inherit',
16
- // lineHeight: 'inherit',
17
- ...(multiline ? { resize: 'none' } : { height: '100%' }),
16
+ flex: 1,
17
+ height: '100%',
18
+ ...(multiline ? { paddingTop: spaces.sm, resize: 'none', flex: 1, textAlignVertical: 'top' } : {}),
18
19
  }
19
20
 
20
21
  return (
@@ -2,8 +2,10 @@ import React from 'react'
2
2
  import dayjs from 'dayjs'
3
3
 
4
4
  import { CalendarNav } from '../../calendar/CalendarNav'
5
+ import { ClearLink } from '../../actions/ClearLink'
5
6
  import { Col } from '../../structure/Col'
6
7
  import { Grid } from '../../structure/Row'
8
+ import { InfiniteCarousel } from '../../carousel/InfiniteCarousel'
7
9
  import { Link } from '../../actions/Link'
8
10
  import { Text } from '../../text/Text'
9
11
  import { View } from '../../structure/View'
@@ -11,33 +13,29 @@ import { WeekDaysBar } from '../../calendar/WeekDaysBar'
11
13
  import { isDateDisabled } from '../../calendar/_helpers/dateDisabled'
12
14
  import { useCalendarDays } from '../../calendar/_helpers/calendarDays'
13
15
 
14
- export function DayPicker({ value, onChange, min, max, onCheckDisabled, ...props }) {
15
- if (!!value) value = dayjs(value)
16
- const [localValue, setLocalValue] = React.useState(value)
17
- const [currentMonth, setCurrentMonth] = React.useState(() => dayjs(value || undefined).startOf('month'))
18
- value = value === undefined ? localValue : value
19
-
20
- React.useEffect(() => {
21
- setLocalValue(value)
22
- if (value?.isValid?.()) setCurrentMonth(value.startOf('month'))
23
- }, [value?.day?.(), value?.month?.(), value?.year?.()])
16
+ function toMonthValue(date) {
17
+ return date.year() * 12 + date.month()
18
+ }
24
19
 
25
- const handleChange = (v) => {
26
- setLocalValue(v)
27
- onChange?.(v)
28
- }
20
+ function fromMonthValue(v) {
21
+ return dayjs()
22
+ .year(Math.floor(v / 12))
23
+ .month(v % 12)
24
+ .startOf('month')
25
+ }
29
26
 
30
- const { cells } = useCalendarDays(currentMonth)
27
+ const MonthDays = React.memo(function MonthDays({ monthValue, selectedKey, onSelect, min, max, onCheckDisabled }) {
28
+ const month = fromMonthValue(monthValue)
29
+ const selectedValue = selectedKey ? dayjs(selectedKey) : null
30
+ const { cells } = useCalendarDays(month)
31
31
 
32
32
  return (
33
- <View className="neko-day-picker" width={275} {...props}>
34
- <CalendarNav value={currentMonth} onChange={setCurrentMonth} />
33
+ <View>
35
34
  <WeekDaysBar />
36
-
37
35
  <Grid className="neko-day-picker-days" colSpan={24 / 7} gap="sm">
38
36
  {cells.map((day, i) => {
39
- const dateVal = currentMonth.date(day)
40
- const isActive = !!value && !!day && dateVal.isSame(value, 'day')
37
+ const dateVal = month.date(day)
38
+ const isActive = !!selectedValue && !!day && dateVal.isSame(selectedValue, 'day')
41
39
  const disabled = isDateDisabled(dateVal, { min, max, onCheckDisabled })
42
40
 
43
41
  return (
@@ -47,7 +45,7 @@ export function DayPicker({ value, onChange, min, max, onCheckDisabled, ...props
47
45
  fullW
48
46
  center
49
47
  br="md"
50
- onPress={() => !!day && handleChange(dateVal)}
48
+ onPress={() => !!day && onSelect(dateVal)}
51
49
  bg={isActive && 'primary'}
52
50
  disabled={disabled}
53
51
  >
@@ -61,4 +59,54 @@ export function DayPicker({ value, onChange, min, max, onCheckDisabled, ...props
61
59
  </Grid>
62
60
  </View>
63
61
  )
62
+ })
63
+
64
+ export function DayPicker({ value, onChange, min, max, onCheckDisabled, allowClear, ...props }) {
65
+ if (!!value) value = dayjs(value)
66
+ const [localValue, setLocalValue] = React.useState(value)
67
+ const [currentMonth, setCurrentMonth] = React.useState(() => dayjs(value || undefined).startOf('month'))
68
+ value = value === undefined ? localValue : value
69
+
70
+ React.useEffect(() => {
71
+ setLocalValue(value)
72
+ if (value?.isValid?.()) setCurrentMonth(value.startOf('month'))
73
+ }, [value?.day?.(), value?.month?.(), value?.year?.()])
74
+
75
+ const handleChange = React.useCallback(
76
+ (v) => {
77
+ setLocalValue(v)
78
+ onChange?.(v)
79
+ },
80
+ [onChange]
81
+ )
82
+
83
+ const monthValue = toMonthValue(currentMonth)
84
+ const minMonth = min ? toMonthValue(dayjs(min).startOf('month')) : undefined
85
+ const maxMonth = max ? toMonthValue(dayjs(max).startOf('month')) : undefined
86
+ const selectedKey = value?.valueOf?.()
87
+
88
+ const renderSlide = (v) => (
89
+ <MonthDays
90
+ monthValue={v}
91
+ selectedKey={selectedKey}
92
+ onSelect={handleChange}
93
+ min={min}
94
+ max={max}
95
+ onCheckDisabled={onCheckDisabled}
96
+ />
97
+ )
98
+
99
+ return (
100
+ <View className="neko-day-picker" width={275} maxW={350} {...props}>
101
+ <CalendarNav value={currentMonth} onChange={setCurrentMonth} />
102
+ <InfiniteCarousel
103
+ value={monthValue}
104
+ onChange={(v) => setCurrentMonth(fromMonthValue(v))}
105
+ renderSlide={renderSlide}
106
+ min={minMonth}
107
+ max={maxMonth}
108
+ />
109
+ <ClearLink hide={!allowClear} value={value} onChange={onChange} />
110
+ </View>
111
+ )
64
112
  }
@@ -2,9 +2,11 @@ import React from 'react'
2
2
  import dayjs from 'dayjs'
3
3
 
4
4
  import { CalendarNav } from '../../calendar/CalendarNav'
5
+ import { ClearLink } from '../../actions/ClearLink'
5
6
  import { Col } from '../../structure/Col'
6
7
  import { Divider } from '../../helpers'
7
8
  import { Grid } from '../../structure/Row'
9
+ import { InfiniteCarousel } from '../../carousel/InfiniteCarousel'
8
10
  import { Link } from '../../actions/Link'
9
11
  import { Text } from '../../text/Text'
10
12
  import { View } from '../../structure/View'
@@ -12,7 +14,39 @@ import { isDateDisabled } from '../../calendar/_helpers/dateDisabled'
12
14
 
13
15
  const months = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
14
16
 
15
- export function MonthPicker({ value, onChange, min, max, onCheckDisabled, ...props }) {
17
+ const MonthGrid = React.memo(function MonthGrid({ year, selectedKey, onSelect, min, max, onCheckDisabled }) {
18
+ const yearDate = dayjs().year(year).startOf('year')
19
+ const selectedValue = selectedKey ? dayjs(selectedKey) : null
20
+
21
+ return (
22
+ <Grid colSpan={8} gap="xs">
23
+ {months.map((month) => {
24
+ const dateVal = yearDate.month(month)
25
+ const isActive = !!selectedValue && dateVal.isSame(selectedValue, 'month')
26
+ const disabled = isDateDisabled(dateVal, { min, max, onCheckDisabled })
27
+
28
+ return (
29
+ <Col key={month}>
30
+ <Link
31
+ fullW
32
+ br="md"
33
+ padding="sm"
34
+ onPress={() => onSelect(dateVal)}
35
+ bg={isActive && 'primary'}
36
+ disabled={disabled}
37
+ >
38
+ <Text text2={!isActive} strong={isActive} center>
39
+ {dateVal.format('MMM')}
40
+ </Text>
41
+ </Link>
42
+ </Col>
43
+ )
44
+ })}
45
+ </Grid>
46
+ )
47
+ })
48
+
49
+ export function MonthPicker({ value, onChange, min, max, onCheckDisabled, allowClear, ...props }) {
16
50
  const [localValue, setLocalValue] = React.useState(value)
17
51
  const [currentYear, setCurrentYear] = React.useState(() => dayjs(value || undefined).startOf('year'))
18
52
  value = value === undefined ? localValue : value
@@ -22,41 +56,36 @@ export function MonthPicker({ value, onChange, min, max, onCheckDisabled, ...pro
22
56
  if (value?.isValid?.()) setCurrentYear(value.startOf('year'))
23
57
  }, [value?.month?.(), value?.year?.()])
24
58
 
25
- const handleChange = (v) => {
26
- const newValue = v.startOf('month')
27
- setLocalValue(newValue)
28
- onChange?.(newValue)
29
- }
59
+ const handleChange = React.useCallback(
60
+ (v) => {
61
+ const newValue = v.startOf('month')
62
+ setLocalValue(newValue)
63
+ onChange?.(newValue)
64
+ },
65
+ [onChange]
66
+ )
67
+
68
+ const yearValue = currentYear.year()
69
+ const minYear = min ? dayjs(min).year() : undefined
70
+ const maxYear = max ? dayjs(max).year() : undefined
71
+ const selectedKey = value?.valueOf?.()
72
+
73
+ const renderSlide = (v) => (
74
+ <MonthGrid year={v} selectedKey={selectedKey} onSelect={handleChange} min={min} max={max} onCheckDisabled={onCheckDisabled} />
75
+ )
30
76
 
31
77
  return (
32
78
  <View className="neko-day-picker" width={275} {...props}>
33
79
  <CalendarNav value={currentYear} onChange={setCurrentYear} level="year" />
34
80
  <Divider />
35
-
36
- <Grid colSpan={8} gap="xs">
37
- {months.map((month) => {
38
- const dateVal = currentYear.month(month)
39
- const isActive = !!value && dateVal.isSame(value, 'week')
40
- const disabled = isDateDisabled(dateVal, { min, max, onCheckDisabled })
41
-
42
- return (
43
- <Col key={month}>
44
- <Link
45
- fullW
46
- br="md"
47
- padding="sm"
48
- onPress={() => handleChange(dateVal)}
49
- bg={isActive && 'primary'}
50
- disabled={disabled}
51
- >
52
- <Text text2={!isActive} strong={isActive} center>
53
- {dateVal.format('MMM')}
54
- </Text>
55
- </Link>
56
- </Col>
57
- )
58
- })}
59
- </Grid>
81
+ <InfiniteCarousel
82
+ value={yearValue}
83
+ onChange={(v) => setCurrentYear(dayjs().year(v).startOf('year'))}
84
+ renderSlide={renderSlide}
85
+ min={minYear}
86
+ max={maxYear}
87
+ />
88
+ <ClearLink hide={!allowClear} value={value} onChange={onChange} />
60
89
  </View>
61
90
  )
62
91
  }