@neko-os/ui 0.3.0 → 0.5.0

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/TouchableOpacity.native.js +8 -2
  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 +110 -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/LinkInput.js +1 -1
  22. package/dist/components/inputs/Picker.js +1 -1
  23. package/dist/components/inputs/TextInput.js +7 -6
  24. package/dist/components/inputs/datePicker/DayPicker.js +65 -23
  25. package/dist/components/inputs/datePicker/MonthPicker.js +51 -27
  26. package/dist/components/inputs/datePicker/QuarterPicker.js +52 -28
  27. package/dist/components/inputs/datePicker/WeekPicker.js +59 -24
  28. package/dist/components/inputs/datePicker/YearPicker.js +59 -35
  29. package/dist/components/keyboard/KeyboardDismissButton.js +3 -0
  30. package/dist/components/keyboard/KeyboardDismissButton.native.js +38 -0
  31. package/dist/components/keyboard/index.js +1 -0
  32. package/dist/components/modals/bottomDrawer/native/BottomDrawer.js +28 -7
  33. package/dist/components/presentation/LabelValue.js +1 -1
  34. package/dist/components/presentation/Result.js +11 -3
  35. package/dist/components/structure/KeyboardAvoidingView.js +9 -2
  36. package/dist/components/theme/ThemePicker.js +7 -12
  37. package/dist/components/theme/ThemeThumb.js +1 -1
  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/TouchableOpacity.native.js +8 -2
  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 +110 -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/LinkInput.js +1 -1
  61. package/src/components/inputs/Picker.js +1 -1
  62. package/src/components/inputs/TextInput.js +5 -4
  63. package/src/components/inputs/datePicker/DayPicker.js +63 -21
  64. package/src/components/inputs/datePicker/MonthPicker.js +50 -26
  65. package/src/components/inputs/datePicker/QuarterPicker.js +50 -26
  66. package/src/components/inputs/datePicker/WeekPicker.js +57 -22
  67. package/src/components/inputs/datePicker/YearPicker.js +58 -34
  68. package/src/components/keyboard/KeyboardDismissButton.js +3 -0
  69. package/src/components/keyboard/KeyboardDismissButton.native.js +38 -0
  70. package/src/components/keyboard/index.js +1 -0
  71. package/src/components/modals/bottomDrawer/native/BottomDrawer.js +27 -6
  72. package/src/components/presentation/LabelValue.js +1 -1
  73. package/src/components/presentation/Result.js +10 -2
  74. package/src/components/structure/KeyboardAvoidingView.js +9 -2
  75. package/src/components/theme/ThemePicker.js +8 -13
  76. package/src/components/theme/ThemeThumb.js +1 -1
  77. package/src/theme/ThemeHandler.js +31 -3
@@ -0,0 +1,9 @@
1
+ import { Keyboard, Pressable } from 'react-native'
2
+
3
+ export function AbsKeyboardDismissView({ children, style }) {
4
+ return (
5
+ <Pressable onPress={Keyboard.dismiss} accessible={false} style={style}>
6
+ {children}
7
+ </Pressable>
8
+ )
9
+ }
@@ -1,3 +1,9 @@
1
- import { TouchableOpacity as RNTouchableOpacity } from 'react-native'
1
+ import { Linking, TouchableOpacity as RNTouchableOpacity } from 'react-native'
2
2
 
3
- export const AbsTouchableOpacity = RNTouchableOpacity
3
+ export function AbsTouchableOpacity({ href, target, link, onPress, ...props }) {
4
+ const handlePress = href
5
+ ? (e) => { onPress?.(e); Linking.openURL(href) }
6
+ : onPress
7
+
8
+ return <RNTouchableOpacity onPress={handlePress} {...props} />
9
+ }
@@ -0,0 +1,6 @@
1
+ import { Link } from './Link'
2
+
3
+ export function ClearLink({ hide, value, onChange, ...props }) {
4
+ if (!!hide || !!value) return false
5
+ return <Link center red label="Remove Value" onPress={() => onChange(null)} padding="sm" {...props} />
6
+ }
@@ -14,7 +14,7 @@ export function FloatingMenu({ fixed, onChange, items, activeIndex, size = 'md',
14
14
 
15
15
  return (
16
16
  <View absolute={!fixed} fixed={fixed} left="md" right="md" centerH bottom={Math.max(insets.bottom / 2, 16)}>
17
- <WrapperView height={height} shadow round row paddingH="sm" bg={bg} {...props}>
17
+ <WrapperView height={height} shadow round row paddingH="sm" bg={bg} border="overlayDivider" {...props}>
18
18
  {items.map((item, index) => {
19
19
  const isActive = index === activeIndex
20
20
 
@@ -20,7 +20,7 @@ export function CalendarNav({ value, onChange, level, ...props }) {
20
20
 
21
21
  return (
22
22
  <View className="neko-calendar-nav" row centerV gap="xxs" height={30} {...props}>
23
- <Link onPress={prevDecade} aria-label="Previous decade" padding="xxs" paddingL={0}>
23
+ <Link onPress={prevDecade} aria-label="Previous decade" padding="sm" paddingL={0}>
24
24
  <Icon name="arrow-left-double-line" />
25
25
  </Link>
26
26
 
@@ -28,7 +28,7 @@ export function CalendarNav({ value, onChange, level, ...props }) {
28
28
  {year}-{year + 9}
29
29
  </Text>
30
30
 
31
- <Link onPress={nextDecade} aria-label="Next decade" padding="xxs" paddingR={0}>
31
+ <Link onPress={nextDecade} aria-label="Next decade" padding="sm" paddingR={0}>
32
32
  <Icon name="arrow-right-double-line" />
33
33
  </Link>
34
34
  </View>
@@ -37,12 +37,12 @@ export function CalendarNav({ value, onChange, level, ...props }) {
37
37
 
38
38
  return (
39
39
  <View className="neko-calendar-nav" row centerV gap="xxs" height={30} {...props}>
40
- <Link onPress={prevYear} aria-label="Previous year" padding="xxs" paddingL={0}>
40
+ <Link onPress={prevYear} aria-label="Previous year" padding="sm" paddingR={0}>
41
41
  <Icon name="arrow-left-double-line" />
42
42
  </Link>
43
43
 
44
44
  {showMonth && (
45
- <Link onPress={prevMonth} aria-label="Previous month" padding="xxs">
45
+ <Link onPress={prevMonth} aria-label="Previous month" padding="sm">
46
46
  <Icon name="arrow-left-s-line" />
47
47
  </Link>
48
48
  )}
@@ -54,12 +54,12 @@ export function CalendarNav({ value, onChange, level, ...props }) {
54
54
  </View>
55
55
 
56
56
  {showMonth && (
57
- <Link onPress={nextMonth} aria-label="Next month" padding="xxs">
57
+ <Link onPress={nextMonth} aria-label="Next month" padding="sm">
58
58
  <Icon name="arrow-right-s-line" />
59
59
  </Link>
60
60
  )}
61
61
 
62
- <Link onPress={nextYear} aria-label="Next year" padding="xxs" paddingR={0}>
62
+ <Link onPress={nextYear} aria-label="Next year" padding="sm" paddingL={0}>
63
63
  <Icon name="arrow-right-double-line" />
64
64
  </Link>
65
65
  </View>
@@ -0,0 +1,48 @@
1
+ import { View } from '../structure/View'
2
+ import { CarouselArrows } from './CarouselArrows'
3
+ import { CarouselDots } from './CarouselDots'
4
+ import { CarouselHandler } from './CarouselHandler'
5
+ import { CarouselSlider } from './CarouselSlider'
6
+
7
+ export function Carousel({
8
+ items,
9
+ activeIndex,
10
+ activeKey,
11
+ onChange,
12
+ afterChange,
13
+ autoplay,
14
+ autoplaySpeed,
15
+ draggable,
16
+ loop,
17
+ showDots,
18
+ showArrows,
19
+ floatingDots,
20
+ dotsProps,
21
+ arrowsProps,
22
+ children,
23
+ ...rootProps
24
+ }) {
25
+ return (
26
+ <CarouselHandler
27
+ items={items}
28
+ activeIndex={activeIndex}
29
+ activeKey={activeKey}
30
+ onChange={onChange}
31
+ afterChange={afterChange}
32
+ autoplay={autoplay}
33
+ autoplaySpeed={autoplaySpeed}
34
+ draggable={draggable}
35
+ loop={loop}
36
+ >
37
+ <View {...rootProps}>
38
+ <View relative>
39
+ <CarouselSlider />
40
+ {showArrows && <CarouselArrows {...arrowsProps} />}
41
+ {showDots && floatingDots && <CarouselDots absolute bottom="xs" left={0} right={0} {...dotsProps} />}
42
+ </View>
43
+ {showDots && !floatingDots && <CarouselDots {...dotsProps} />}
44
+ {children}
45
+ </View>
46
+ </CarouselHandler>
47
+ )
48
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+
3
+ import { Icon } from '../presentation/Icon'
4
+ import { Link } from '../actions/Link'
5
+ import { View } from '../structure/View'
6
+ import { useCarousel } from './CarouselHandler'
7
+
8
+ export function CarouselArrows({ iconSize = 'md', ...props }) {
9
+ const { goToNext, goToPrev, activeIndex, itemsCount, loop } = useCarousel()
10
+
11
+ const showPrev = loop || activeIndex > 0
12
+ const showNext = loop || activeIndex < itemsCount - 1
13
+
14
+ return (
15
+ <View
16
+ absoluteFill
17
+ row
18
+ centerV
19
+ paddingH="xs"
20
+ justify="space-between"
21
+ style={{ pointerEvents: 'none' }}
22
+ {...props}
23
+ >
24
+ {showPrev ? (
25
+ <Link onPress={goToPrev} bg="overlayBG_op80" round padding="xs" shadow style={{ pointerEvents: 'auto' }}>
26
+ <Icon name="arrow-left-s-line" text3 size={iconSize} />
27
+ </Link>
28
+ ) : (
29
+ <View />
30
+ )}
31
+ {showNext ? (
32
+ <Link onPress={goToNext} bg="overlayBG_op80" round padding="xs" shadow style={{ pointerEvents: 'auto' }}>
33
+ <Icon name="arrow-right-s-line" text3 size={iconSize} />
34
+ </Link>
35
+ ) : (
36
+ <View />
37
+ )}
38
+ </View>
39
+ )
40
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+
3
+ import { Icon } from '../presentation/Icon'
4
+ import { Link } from '../actions/Link'
5
+ import { View } from '../structure/View'
6
+ import { useCarousel } from './CarouselHandler'
7
+
8
+ export function CarouselArrows({ iconSize = 'md', ...props }) {
9
+ const { goToNext, goToPrev, activeIndex, itemsCount, loop } = useCarousel()
10
+
11
+ const showPrev = loop || activeIndex > 0
12
+ const showNext = loop || activeIndex < itemsCount - 1
13
+
14
+ return (
15
+ <View
16
+ absoluteFill
17
+ row
18
+ centerV
19
+ paddingH="xs"
20
+ justify="space-between"
21
+ pointerEvents="box-none"
22
+ {...props}
23
+ >
24
+ {showPrev ? (
25
+ <Link onPress={goToPrev} bg="overlayBG_op80" round padding="xs" shadow>
26
+ <Icon name="arrow-left-s-line" text3 size={iconSize} />
27
+ </Link>
28
+ ) : (
29
+ <View />
30
+ )}
31
+ {showNext ? (
32
+ <Link onPress={goToNext} bg="overlayBG_op80" round padding="xs" shadow>
33
+ <Icon name="arrow-right-s-line" text3 size={iconSize} />
34
+ </Link>
35
+ ) : (
36
+ <View />
37
+ )}
38
+ </View>
39
+ )
40
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react'
2
+
3
+ import { Pressable } from '../actions/Pressable'
4
+ import { View } from '../structure/View'
5
+ import { useCarousel } from './CarouselHandler'
6
+
7
+ function Dot({ active, onPress }) {
8
+ return (
9
+ <Pressable
10
+ onPress={onPress}
11
+ width={active ? 20 : 8}
12
+ height={8}
13
+ round
14
+ bg={active ? 'primary' : 'text4_op30'}
15
+ style={{ transition: 'all 200ms ease-in-out' }}
16
+ />
17
+ )
18
+ }
19
+
20
+ export function CarouselDots(props) {
21
+ const { items, activeIndex, goTo } = useCarousel()
22
+
23
+ if (!items?.length) return null
24
+
25
+ return (
26
+ <View row center gap="xs" paddingV="sm" {...props}>
27
+ {items.map((item, index) => (
28
+ <Dot key={item.key} active={index === activeIndex} onPress={() => goTo(index)} />
29
+ ))}
30
+ </View>
31
+ )
32
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react'
2
+ import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
3
+
4
+ import { Pressable } from '../actions/Pressable'
5
+ import { View } from '../structure/View'
6
+ import { useColors } from '../../theme/ThemeHandler'
7
+ import { useCarousel } from './CarouselHandler'
8
+
9
+ function Dot({ active, onPress }) {
10
+ const colors = useColors()
11
+
12
+ const animatedStyle = useAnimatedStyle(() => ({
13
+ width: withTiming(active ? 20 : 8, { duration: 200 }),
14
+ backgroundColor: withTiming(active ? colors.primary : colors.text4_op30, { duration: 200 }),
15
+ }), [active])
16
+
17
+ return (
18
+ <Pressable onPress={onPress}>
19
+ <Animated.View style={[{ height: 8, borderRadius: 4 }, animatedStyle]} />
20
+ </Pressable>
21
+ )
22
+ }
23
+
24
+ export function CarouselDots(props) {
25
+ const { items, activeIndex, goTo } = useCarousel()
26
+
27
+ if (!items?.length) return null
28
+
29
+ return (
30
+ <View row center gap="xs" paddingV="sm" {...props}>
31
+ {items.map((item, index) => (
32
+ <Dot key={item.key} active={index === activeIndex} onPress={() => goTo(index)} />
33
+ ))}
34
+ </View>
35
+ )
36
+ }
@@ -0,0 +1,86 @@
1
+ import React from 'react'
2
+
3
+ const CarouselContext = React.createContext(null)
4
+ export const useCarousel = () => React.useContext(CarouselContext) || {}
5
+
6
+ export function CarouselHandler({
7
+ children,
8
+ items,
9
+ activeIndex: controlledIndex,
10
+ activeKey,
11
+ onChange,
12
+ afterChange,
13
+ autoplay,
14
+ autoplaySpeed = 3000,
15
+ draggable,
16
+ loop,
17
+ }) {
18
+ const itemsCount = items?.length || 0
19
+ const isControlled = controlledIndex !== undefined || activeKey !== undefined
20
+
21
+ const [internalIndex, setInternalIndex] = React.useState(() => {
22
+ if (controlledIndex !== undefined) return controlledIndex
23
+ if (activeKey !== undefined) return Math.max(0, items?.findIndex((i) => i.key === activeKey) || 0)
24
+ return 0
25
+ })
26
+
27
+ const resolvedIndex = isControlled
28
+ ? activeKey !== undefined
29
+ ? Math.max(0, items?.findIndex((i) => i.key === activeKey) || 0)
30
+ : controlledIndex
31
+ : internalIndex
32
+
33
+ const activeItem = React.useMemo(() => items?.[resolvedIndex], [items, resolvedIndex])
34
+
35
+ const pausedRef = React.useRef(false)
36
+
37
+ const goTo = React.useCallback(
38
+ (index) => {
39
+ let next = index
40
+ if (loop) next = ((next % itemsCount) + itemsCount) % itemsCount
41
+ else next = Math.max(0, Math.min(next, itemsCount - 1))
42
+
43
+ if (!isControlled) setInternalIndex(next)
44
+ onChange?.(items?.[next]?.key, next)
45
+ },
46
+ [loop, itemsCount, isControlled, onChange, items]
47
+ )
48
+
49
+ const goToNext = React.useCallback(() => goTo(resolvedIndex + 1), [goTo, resolvedIndex])
50
+ const goToPrev = React.useCallback(() => goTo(resolvedIndex - 1), [goTo, resolvedIndex])
51
+
52
+ const pauseAutoplay = React.useCallback(() => {
53
+ pausedRef.current = true
54
+ }, [])
55
+
56
+ const resumeAutoplay = React.useCallback(() => {
57
+ pausedRef.current = false
58
+ }, [])
59
+
60
+ React.useEffect(() => {
61
+ if (!autoplay || itemsCount <= 1) return
62
+
63
+ const interval = setInterval(() => {
64
+ if (!pausedRef.current) goToNext()
65
+ }, autoplaySpeed)
66
+
67
+ return () => clearInterval(interval)
68
+ }, [autoplay, autoplaySpeed, itemsCount, resolvedIndex, goToNext])
69
+
70
+ const value = {
71
+ items,
72
+ activeIndex: resolvedIndex,
73
+ activeItem,
74
+ itemsCount,
75
+ goTo,
76
+ goToNext,
77
+ goToPrev,
78
+ afterChange,
79
+ draggable,
80
+ loop,
81
+ pauseAutoplay,
82
+ resumeAutoplay,
83
+ }
84
+
85
+ return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
86
+ }
@@ -0,0 +1,124 @@
1
+ import React from 'react'
2
+
3
+ import { View } from '../structure/View'
4
+ import { useCarousel } from './CarouselHandler'
5
+
6
+ function SlideContent({ item }) {
7
+ const Content = React.useMemo(
8
+ () => item.render || item.renderContent || item.Content,
9
+ [item.render, item.renderContent, item.Content]
10
+ )
11
+ return Content ? <Content /> : null
12
+ }
13
+
14
+ export function CarouselSlider() {
15
+ const { items, activeIndex, itemsCount, draggable, loop, afterChange, goToNext, goToPrev, pauseAutoplay, resumeAutoplay } =
16
+ useCarousel()
17
+
18
+ const containerRef = React.useRef(null)
19
+ const [isDragging, setIsDragging] = React.useState(false)
20
+ const [dragOffset, setDragOffset] = React.useState(0)
21
+ const startXRef = React.useRef(0)
22
+ const startTimeRef = React.useRef(0)
23
+ const prevItemsRef = React.useRef(items)
24
+ const skipTransitionRef = React.useRef(false)
25
+
26
+ if (items !== prevItemsRef.current) {
27
+ skipTransitionRef.current = true
28
+ prevItemsRef.current = items
29
+ }
30
+
31
+ React.useEffect(() => {
32
+ if (skipTransitionRef.current) {
33
+ const id = requestAnimationFrame(() => {
34
+ skipTransitionRef.current = false
35
+ })
36
+ return () => cancelAnimationFrame(id)
37
+ }
38
+ })
39
+
40
+ if (!items?.length) return null
41
+
42
+ const baseTranslate = -(activeIndex * 100) / itemsCount
43
+ const dragPercent =
44
+ isDragging && containerRef.current ? (dragOffset / containerRef.current.offsetWidth) * (100 / itemsCount) : 0
45
+ const transformX = baseTranslate + dragPercent
46
+
47
+ const dragStateRef = React.useRef({ isDragging: false, dragOffset: 0 })
48
+
49
+ const handlePointerDown = (e) => {
50
+ if (!draggable) return
51
+ startXRef.current = e.clientX
52
+ startTimeRef.current = Date.now()
53
+ dragStateRef.current = { isDragging: true, dragOffset: 0 }
54
+ setIsDragging(true)
55
+ setDragOffset(0)
56
+ pauseAutoplay()
57
+
58
+ const onMove = (ev) => {
59
+ const raw = ev.clientX - startXRef.current
60
+ let offset = raw
61
+ if (!loop) {
62
+ const containerWidth = containerRef.current?.offsetWidth || 1
63
+ const atStart = activeIndex === 0 && raw > 0
64
+ const atEnd = activeIndex === itemsCount - 1 && raw < 0
65
+ if (atStart || atEnd) offset = raw * 0.3
66
+ }
67
+ dragStateRef.current.dragOffset = offset
68
+ setDragOffset(offset)
69
+ }
70
+
71
+ const onUp = () => {
72
+ window.removeEventListener('pointermove', onMove)
73
+ window.removeEventListener('pointerup', onUp)
74
+ window.removeEventListener('pointercancel', onUp)
75
+
76
+ const offset = dragStateRef.current.dragOffset
77
+ const containerWidth = containerRef.current?.offsetWidth || 1
78
+ const threshold = containerWidth * 0.25
79
+ const elapsed = Date.now() - startTimeRef.current
80
+ const velocity = Math.abs(offset) / (elapsed || 1)
81
+
82
+ if (offset < -threshold || (offset < 0 && velocity > 0.5)) {
83
+ goToNext()
84
+ } else if (offset > threshold || (offset > 0 && velocity > 0.5)) {
85
+ goToPrev()
86
+ }
87
+
88
+ dragStateRef.current = { isDragging: false, dragOffset: 0 }
89
+ setIsDragging(false)
90
+ setDragOffset(0)
91
+ resumeAutoplay()
92
+ }
93
+
94
+ window.addEventListener('pointermove', onMove)
95
+ window.addEventListener('pointerup', onUp)
96
+ window.addEventListener('pointercancel', onUp)
97
+ }
98
+
99
+ return (
100
+ <View hiddenOverflow fullW ref={containerRef}>
101
+ <View
102
+ row
103
+ style={{
104
+ width: `${itemsCount * 100}%`,
105
+ transform: `translateX(${transformX}%)`,
106
+ transition: isDragging || skipTransitionRef.current ? 'none' : 'transform 300ms ease-in-out',
107
+ touchAction: 'pan-y',
108
+ cursor: draggable ? (isDragging ? 'grabbing' : 'grab') : undefined,
109
+ userSelect: 'none',
110
+ }}
111
+ onTransitionEnd={afterChange ? (e) => {
112
+ if (e.propertyName === 'transform') afterChange(items?.[activeIndex]?.key, activeIndex)
113
+ } : undefined}
114
+ onPointerDown={draggable ? handlePointerDown : undefined}
115
+ >
116
+ {items.map((item) => (
117
+ <View key={item.key} style={{ width: `${100 / itemsCount}%` }}>
118
+ <SlideContent item={item} />
119
+ </View>
120
+ ))}
121
+ </View>
122
+ </View>
123
+ )
124
+ }
@@ -0,0 +1,110 @@
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
+
31
+ React.useEffect(() => {
32
+ if (slideWidth > 0) {
33
+ if (prevItemsRef.current !== items) {
34
+ prevItemsRef.current = items
35
+ translateX.value = -activeIndex * slideWidth
36
+ } else {
37
+ translateX.value = withTiming(-activeIndex * slideWidth, { duration: 300 }, (finished) => {
38
+ if (finished && afterChange) runOnJS(afterChange)(items?.[activeIndex]?.key, activeIndex)
39
+ })
40
+ }
41
+ }
42
+ }, [activeIndex, slideWidth, items])
43
+
44
+ const onLayout = React.useCallback((e) => {
45
+ setSlideWidth(e.nativeEvent.layout.width)
46
+ }, [])
47
+
48
+ const animatedStyle = useAnimatedStyle(() => ({
49
+ transform: [{ translateX: translateX.value }],
50
+ }))
51
+
52
+ if (!items?.length) return null
53
+
54
+ const minTranslate = -(itemsCount - 1) * slideWidth
55
+ const maxTranslate = 0
56
+
57
+ const panGesture = Gesture.Pan()
58
+ .activeOffsetX([-10, 10])
59
+ .failOffsetY([-10, 10])
60
+ .onStart(() => {
61
+ gestureStartX.value = translateX.value
62
+ runOnJS(pauseAutoplay)()
63
+ })
64
+ .onUpdate((e) => {
65
+ const raw = gestureStartX.value + e.translationX
66
+ if (loop) {
67
+ translateX.value = raw
68
+ } else {
69
+ // Rubber band resistance at edges
70
+ if (raw > maxTranslate) {
71
+ translateX.value = maxTranslate + (raw - maxTranslate) * 0.3
72
+ } else if (raw < minTranslate) {
73
+ translateX.value = minTranslate + (raw - minTranslate) * 0.3
74
+ } else {
75
+ translateX.value = raw
76
+ }
77
+ }
78
+ })
79
+ .onEnd((e) => {
80
+ const threshold = slideWidth * 0.25
81
+ let targetIndex = activeIndex
82
+
83
+ if (e.translationX < -threshold || e.velocityX < -500) {
84
+ targetIndex = activeIndex + 1
85
+ } else if (e.translationX > threshold || e.velocityX > 500) {
86
+ targetIndex = activeIndex - 1
87
+ }
88
+
89
+ const clamped = clampIndex(targetIndex, itemsCount, loop)
90
+ translateX.value = withTiming(-clamped * slideWidth, { duration: 300 })
91
+ runOnJS(goTo)(targetIndex)
92
+ runOnJS(resumeAutoplay)()
93
+ })
94
+
95
+ const track = (
96
+ <Animated.View style={[{ flexDirection: 'row', width: slideWidth * itemsCount }, animatedStyle]}>
97
+ {items.map((item) => (
98
+ <View key={item.key} style={{ width: slideWidth }}>
99
+ <SlideContent item={item} />
100
+ </View>
101
+ ))}
102
+ </Animated.View>
103
+ )
104
+
105
+ return (
106
+ <View hiddenOverflow fullW onLayout={onLayout}>
107
+ {draggable ? <GestureDetector gesture={panGesture}>{track}</GestureDetector> : track}
108
+ </View>
109
+ )
110
+ }
@@ -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>