@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,51 @@
1
+ import { pipe } from 'ramda'
2
+ import React from 'react'
3
+
4
+ import { View } from './View'
5
+ import { useDefaultModifier } from '../../modifiers/default'
6
+ import { useSizeConverter } from '../../modifiers/sizeConverter'
7
+ import { useThemeComponentModifier } from '../../modifiers/themeComponent'
8
+
9
+ const DEFAULT_PROPS = ([{ sizeCode }]) => ({
10
+ row: true,
11
+ justify: 'stretch',
12
+ br: sizeCode,
13
+ // overflow: 'hidden',
14
+ })
15
+
16
+ export function Segment({ children, ...rootProps }) {
17
+ const [{ sizeCode }, formattedProps] = pipe(
18
+ useSizeConverter('elementHeights', 'md'),
19
+ useThemeComponentModifier('Segment'),
20
+ useDefaultModifier(DEFAULT_PROPS)
21
+ )([{}, rootProps])
22
+
23
+ const { br, ...props } = formattedProps
24
+ const size = children?.length
25
+
26
+ return (
27
+ <View className="neko-segment" {...props}>
28
+ {React.Children.map(children, (child, index) => {
29
+ if (!React.isValidElement(child)) return child
30
+ const isFirst = index === 0
31
+ const isLast = size - 1 === index
32
+
33
+ const childProps = child.props || {}
34
+ const newProps = {
35
+ brL: br,
36
+ brR: br,
37
+ size: sizeCode,
38
+ }
39
+
40
+ if (!isLast) {
41
+ newProps.brR = 0
42
+ }
43
+ if (!isFirst) {
44
+ newProps.brL = 0
45
+ }
46
+
47
+ return React.cloneElement(child, newProps)
48
+ })}
49
+ </View>
50
+ )
51
+ }
@@ -53,7 +53,10 @@ function InnerContent({
53
53
  const snapIndex = useSharedValue(0)
54
54
  const velocityY = useSharedValue(0)
55
55
 
56
- const normalizedSnapPoints = React.useMemo(() => normalizeSnapPoints(snapPoints, SCREEN_HEIGHT), [snapPoints])
56
+ const normalizedSnapPoints = React.useMemo(
57
+ () => normalizeSnapPoints(snapPoints, SCREEN_HEIGHT, bottomInset),
58
+ [snapPoints, useSafeArea]
59
+ )
57
60
  const maxSnapPoint = React.useMemo(() => Math.max(...normalizedSnapPoints), [normalizedSnapPoints])
58
61
  const minSnapPoint = React.useMemo(() => Math.min(...normalizedSnapPoints), [normalizedSnapPoints])
59
62
 
@@ -1,27 +1,32 @@
1
- export function normalizeSnapPoints(snapPoints, screenHeight) {
1
+ import { is } from 'ramda'
2
+
3
+ export function normalizeSnapPoints(snapPoints, screenHeight, bottomInset) {
2
4
  return snapPoints.map((point) => {
3
5
  if (typeof point === 'string' && point.endsWith('%')) {
4
- const percentage = parseFloat(point) / 100;
5
- return screenHeight * percentage;
6
+ const percentage = parseFloat(point) / 100
7
+ return screenHeight * percentage
8
+ }
9
+ if (is(Number, point)) {
10
+ point = point + bottomInset
6
11
  }
7
- return point;
8
- });
12
+ return point
13
+ })
9
14
  }
10
15
 
11
16
  export function findClosestSnapPoint(currentPosition, snapPoints, velocity = 0) {
12
- 'worklet';
17
+ 'worklet'
13
18
  // Reduced velocity impact for more stable snapping
14
- const velocityImpact = velocity * 0.03;
15
- const adjustedPosition = currentPosition + velocityImpact;
19
+ const velocityImpact = velocity * 0.03
20
+ const adjustedPosition = currentPosition + velocityImpact
16
21
 
17
- let closestIndex = 0;
18
- let minDistance = Math.abs(snapPoints[0] - adjustedPosition);
22
+ let closestIndex = 0
23
+ let minDistance = Math.abs(snapPoints[0] - adjustedPosition)
19
24
 
20
25
  for (let i = 1; i < snapPoints.length; i++) {
21
- const distance = Math.abs(snapPoints[i] - adjustedPosition);
26
+ const distance = Math.abs(snapPoints[i] - adjustedPosition)
22
27
  if (distance < minDistance) {
23
- minDistance = distance;
24
- closestIndex = i;
28
+ minDistance = distance
29
+ closestIndex = i
25
30
  }
26
31
  }
27
32
 
@@ -30,29 +35,31 @@ export function findClosestSnapPoint(currentPosition, snapPoints, velocity = 0)
30
35
 
31
36
  // Bias towards opening more when swiping up very fast
32
37
  if (velocity < -1500 && closestIndex < snapPoints.length - 1) {
33
- closestIndex++;
38
+ closestIndex++
34
39
  }
35
40
  // Bias towards closing when swiping down very fast
36
41
  else if (velocity > 1500 && closestIndex > 0) {
37
- closestIndex--;
42
+ closestIndex--
38
43
  }
39
44
 
40
45
  // Add hysteresis: prefer staying at current position unless moved significantly
41
46
  // This prevents accidental snap point changes
42
47
  if (snapPoints.length > 1) {
43
- const currentSnapDistance = minDistance;
48
+ const currentSnapDistance = minDistance
44
49
  // Require at least 20% of the distance between snap points to change
45
- const snapPointSpacing = Math.abs(snapPoints[Math.min(closestIndex + 1, snapPoints.length - 1)] - snapPoints[closestIndex]);
50
+ const snapPointSpacing = Math.abs(
51
+ snapPoints[Math.min(closestIndex + 1, snapPoints.length - 1)] - snapPoints[closestIndex]
52
+ )
46
53
  if (currentSnapDistance < snapPointSpacing * 0.2) {
47
54
  // Stay at current snap point unless moved significantly
48
- return closestIndex;
55
+ return closestIndex
49
56
  }
50
57
  }
51
58
 
52
- return closestIndex;
59
+ return closestIndex
53
60
  }
54
61
 
55
62
  export function clamp(value, min, max) {
56
- 'worklet';
57
- return Math.min(Math.max(value, min), max);
58
- }
63
+ 'worklet'
64
+ return Math.min(Math.max(value, min), max)
65
+ }
@@ -1,3 +1,5 @@
1
1
  import { Drawer } from '../../drawer'
2
2
 
3
- export const BottomDrawer = (props) => <Drawer height={400} {...props} />
3
+ export const BottomDrawer = ({ snapPoints, contentProps, ...props }) => {
4
+ return <Drawer height={snapPoints?.[0] || 400} contentProps={{ padding: 0, ...contentProps }} {...props} />
5
+ }
@@ -11,3 +11,4 @@ export * from './modal'
11
11
  export * from './drawer'
12
12
  export * from './bottomDrawer'
13
13
  export * from './popover/Popover'
14
+ export * from './Segment'
@@ -29,6 +29,11 @@ export const useRegisterOverlay = (opts = {}) => {
29
29
  mergeOverlay({ open: true, content, triggerRect, placement, ...options })
30
30
  }
31
31
 
32
+ const onUpdate = ({ content, options = {} }) => {
33
+ if (!overlay.open) return
34
+ mergeOverlay({ content, ...options })
35
+ }
36
+
32
37
  const onClose = () => {
33
38
  stopDelayedClosing()
34
39
  timeout.current = setTimeout(() => {
@@ -41,7 +46,7 @@ export const useRegisterOverlay = (opts = {}) => {
41
46
  !!unmountOnClose ? removeOverlay() : closeOverlay()
42
47
  }
43
48
 
44
- return { onOpen, onClose, onFastClose, stopDelayedClosing }
49
+ return { onOpen, onClose, onUpdate, onFastClose, stopDelayedClosing }
45
50
  }
46
51
 
47
52
  export function OverlayHandler({ children }) {
@@ -16,11 +16,13 @@ export function Popover({
16
16
  parentMinWidth,
17
17
  useBottomDrawer = {},
18
18
  bottomDrawerProps = {},
19
+ watch,
20
+ disabled,
19
21
  ...props
20
22
  }) {
21
23
  const shouldUseDrawer = useResponsiveValue(useBottomDrawer)
22
24
  const ref = React.useRef(null)
23
- const { onOpen, onClose, onFastClose, stopDelayedClosing } = useRegisterOverlay({ unmountOnClose })
25
+ const { onOpen, onUpdate, onClose, onFastClose, stopDelayedClosing } = useRegisterOverlay({ unmountOnClose })
24
26
 
25
27
  const click = trigger === 'click'
26
28
  const hover = trigger === 'hover'
@@ -28,13 +30,24 @@ export function Popover({
28
30
 
29
31
  renderContent = renderContent || (() => content)
30
32
 
31
- const show = (e) => {
32
- if (e && e.stopPropagation) e.stopPropagation()
33
+ const buildContent = () => (
34
+ <PopoverContent
35
+ placement={placement}
36
+ width={parentWidth ? ref.current?.getBoundingClientRect()?.width : undefined}
37
+ minWidth={parentMinWidth ? ref.current?.getBoundingClientRect()?.width : undefined}
38
+ {...props}
39
+ onMouseEnter={hover ? stopDelayedClosing : undefined}
40
+ onMouseLeave={hover ? onClose : undefined}
41
+ >
42
+ {renderContent({ onClose: onFastClose })}
43
+ </PopoverContent>
44
+ )
45
+
46
+ const computeTriggerRect = () => {
33
47
  const rect = ref.current.getBoundingClientRect()
34
48
  const scrollX = window.scrollX || window.pageXOffset
35
49
  const scrollY = window.scrollY || window.pageYOffset
36
-
37
- const triggerRect = {
50
+ return {
38
51
  left: rect.left + scrollX,
39
52
  right: rect.right + scrollX,
40
53
  top: rect.top + scrollY,
@@ -42,20 +55,13 @@ export function Popover({
42
55
  width: rect.width,
43
56
  height: rect.height,
44
57
  }
58
+ }
45
59
 
60
+ const show = (e) => {
61
+ if (e && e.stopPropagation) e.stopPropagation()
62
+ const triggerRect = computeTriggerRect()
46
63
  onOpen({
47
- content: (
48
- <PopoverContent
49
- placement={placement}
50
- width={parentWidth ? rect.width : undefined}
51
- minWidth={parentMinWidth ? rect.width : undefined}
52
- {...props}
53
- onMouseEnter={hover ? stopDelayedClosing : undefined}
54
- onMouseLeave={hover ? onClose : undefined}
55
- >
56
- {renderContent({ onClose: onFastClose })}
57
- </PopoverContent>
58
- ),
64
+ content: buildContent(),
59
65
  triggerRect,
60
66
  placement,
61
67
  options: { dismissOnClickOutside: click || focus },
@@ -64,6 +70,14 @@ export function Popover({
64
70
 
65
71
  React.useEffect(() => () => onClose(), [])
66
72
 
73
+ React.useEffect(() => {
74
+ if (!ref.current || !watch) return
75
+ onUpdate({
76
+ content: buildContent(),
77
+ options: { dismissOnClickOutside: click || focus },
78
+ })
79
+ }, watch)
80
+
67
81
  if (shouldUseDrawer) {
68
82
  return (
69
83
  <DrawerPopover
@@ -79,8 +93,8 @@ export function Popover({
79
93
  const child = React.Children.only(children)
80
94
  let childProps = { ref, onClick: show }
81
95
 
82
- if (hover) childProps = { ref, onMouseEnter: show, onMouseLeave: onClose }
83
- if (focus) childProps = { ref, onFocus: show }
96
+ if (hover) childProps = { ref, onMouseEnter: show, onMouseLeave: onClose, disabled }
97
+ if (focus) childProps = { ref, onFocus: show, disabled }
84
98
 
85
99
  return React.cloneElement(child, childProps)
86
100
  }
@@ -0,0 +1,157 @@
1
+ import React from 'react'
2
+
3
+ import { BottomDrawer } from '../bottomDrawer'
4
+ import { PopoverContent } from './PopoverContent'
5
+ import { useRegisterOverlay } from '../overlay/OverlayHandler'
6
+ import { useResponsiveValue } from '../../../responsive'
7
+
8
+ export function Popover({
9
+ renderContent,
10
+ content,
11
+ trigger = 'hover',
12
+ placement = 'bottom',
13
+ unmountOnClose,
14
+ children,
15
+ parentWidth,
16
+ parentMinWidth,
17
+ useBottomDrawer = {},
18
+ bottomDrawerProps = {},
19
+ watch,
20
+ ...props
21
+ }) {
22
+ const shouldUseDrawer = useResponsiveValue(useBottomDrawer)
23
+ const ref = React.useRef(null)
24
+ const { onOpen, onUpdate, onClose, onFastClose, stopDelayedClosing } = useRegisterOverlay({ unmountOnClose })
25
+
26
+ const click = trigger === 'click'
27
+ const hover = trigger === 'hover'
28
+ const focus = trigger === 'focus'
29
+
30
+ renderContent = renderContent || (() => content)
31
+
32
+ const buildContent = () => (
33
+ <PopoverContent
34
+ placement={placement}
35
+ width={parentWidth ? ref.current?.getBoundingClientRect()?.width : undefined}
36
+ minWidth={parentMinWidth ? ref.current?.getBoundingClientRect()?.width : undefined}
37
+ {...props}
38
+ onMouseEnter={hover ? stopDelayedClosing : undefined}
39
+ onMouseLeave={hover ? onClose : undefined}
40
+ >
41
+ {renderContent({ onClose: onFastClose })}
42
+ </PopoverContent>
43
+ )
44
+
45
+ const computeTriggerRect = () => {
46
+ const rect = ref.current.getBoundingClientRect()
47
+ const scrollX = window.scrollX || window.pageXOffset
48
+ const scrollY = window.scrollY || window.pageYOffset
49
+ return {
50
+ left: rect.left + scrollX,
51
+ right: rect.right + scrollX,
52
+ top: rect.top + scrollY,
53
+ bottom: rect.bottom + scrollY,
54
+ width: rect.width,
55
+ height: rect.height,
56
+ }
57
+ }
58
+
59
+ const show = (e) => {
60
+ if (e && e.stopPropagation) e.stopPropagation()
61
+ const triggerRect = computeTriggerRect()
62
+ onOpen({
63
+ content: buildContent(),
64
+ triggerRect,
65
+ placement,
66
+ options: { dismissOnClickOutside: click || focus },
67
+ })
68
+ }
69
+
70
+ React.useEffect(() => () => onClose(), [])
71
+
72
+ // 🔑 re-render do portal quando `watch` (ex.: search) mudar
73
+ React.useEffect(() => {
74
+ if (!ref.current) return
75
+ const triggerRect = computeTriggerRect()
76
+ onUpdate({
77
+ content: buildContent(),
78
+ triggerRect,
79
+ placement,
80
+ options: { dismissOnClickOutside: click || focus },
81
+ })
82
+ }, [watch, renderContent])
83
+
84
+ // const show = (e) => {
85
+ // if (e && e.stopPropagation) e.stopPropagation()
86
+ // const rect = ref.current.getBoundingClientRect()
87
+ // const scrollX = window.scrollX || window.pageXOffset
88
+ // const scrollY = window.scrollY || window.pageYOffset
89
+
90
+ // const triggerRect = {
91
+ // left: rect.left + scrollX,
92
+ // right: rect.right + scrollX,
93
+ // top: rect.top + scrollY,
94
+ // bottom: rect.bottom + scrollY,
95
+ // width: rect.width,
96
+ // height: rect.height,
97
+ // }
98
+
99
+ // onOpen({
100
+ // content: () => (
101
+ // <PopoverContent
102
+ // placement={placement}
103
+ // width={parentWidth ? rect.width : undefined}
104
+ // minWidth={parentMinWidth ? rect.width : undefined}
105
+ // {...props}
106
+ // onMouseEnter={hover ? stopDelayedClosing : undefined}
107
+ // onMouseLeave={hover ? onClose : undefined}
108
+ // >
109
+ // {renderContent({ onClose: onFastClose })}
110
+ // </PopoverContent>
111
+ // ),
112
+ // triggerRect,
113
+ // placement,
114
+ // options: { dismissOnClickOutside: click || focus },
115
+ // })
116
+ // }
117
+
118
+ // React.useEffect(() => () => onClose(), [])
119
+
120
+ if (shouldUseDrawer) {
121
+ return (
122
+ <DrawerPopover
123
+ content={content}
124
+ renderContent={renderContent}
125
+ children={children}
126
+ {...props}
127
+ {...bottomDrawerProps}
128
+ />
129
+ )
130
+ }
131
+
132
+ const child = React.Children.only(children)
133
+ let childProps = { ref, onClick: show }
134
+
135
+ if (hover) childProps = { ref, onMouseEnter: show, onMouseLeave: onClose }
136
+ if (focus) childProps = { ref, onFocus: show }
137
+
138
+ return React.cloneElement(child, childProps)
139
+ }
140
+
141
+ function DrawerPopover({ children, content, renderContent, snapPoints, ...props }) {
142
+ const [open, setOpen] = React.useState(false)
143
+ const onClose = () => setOpen(false)
144
+
145
+ const child = React.Children.only(children)
146
+ let childProps = { onClick: () => setOpen(true) }
147
+
148
+ return (
149
+ <>
150
+ {React.cloneElement(child, childProps)}
151
+
152
+ <BottomDrawer open={open} onClose={onClose} snapPoints={snapPoints} {...props}>
153
+ {renderContent({ onClose })}
154
+ </BottomDrawer>
155
+ </>
156
+ )
157
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+
3
+ import { AnimatedView } from '../animations'
4
+ import { View } from '../structure'
5
+ import { useTabs } from './TabsHandler'
6
+
7
+ const duration = 100
8
+
9
+ function Item({ item, active, ...props }) {
10
+ const Content = React.useMemo(() => item.renderContent || item.Content, [item.renderContent, item.Content])
11
+ const [open, setOpen] = React.useState(active)
12
+
13
+ React.useEffect(() => {
14
+ if (!!active) {
15
+ setTimeout(() => setOpen(true), duration)
16
+ } else {
17
+ setOpen(active)
18
+ }
19
+ }, [active])
20
+
21
+ return (
22
+ <AnimatedView open={open} fade={{ duration }} flex display={!open && 'none'} {...props}>
23
+ <Content />
24
+ </AnimatedView>
25
+ )
26
+ }
27
+
28
+ export function ActiveTabContent({ lazy, unmountOnClose }) {
29
+ const { activeKey, items } = useTabs()
30
+ if (!items?.length) return false
31
+
32
+ return items.map((item) => (
33
+ <Item key={item.key} item={item} active={item.key === activeKey} lazy={lazy} unmountOnClose={unmountOnClose} />
34
+ ))
35
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react'
2
+
3
+ const TabsContext = React.createContext(null)
4
+ export const useTabs = () => React.useContext(TabsContext) || {}
5
+
6
+ export function TabsHandler({ children, items, initialKey }) {
7
+ const [activeKey, setActiveKey] = React.useState(initialKey || items?.[0]?.key)
8
+
9
+ const activeTab = React.useMemo(() => {
10
+ return items?.find((item) => item.key === activeKey)
11
+ }, [activeKey])
12
+
13
+ const value = { items, onChange: setActiveKey, activeKey, activeTab }
14
+
15
+ return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>
16
+ }
@@ -0,0 +1,15 @@
1
+ import { Menu } from '../actions'
2
+ import { SegmentedPicker } from '../inputs'
3
+ import { useTabs } from './TabsHandler'
4
+
5
+ export function TabsMenu(props) {
6
+ const { activeKey, items, onChange } = useTabs()
7
+
8
+ return <Menu items={items} activeKey={activeKey} onChange={({ key }) => onChange(key)} {...props} />
9
+ }
10
+
11
+ export function TabsSegmentedMenu(props) {
12
+ const { activeKey, items, onChange } = useTabs()
13
+
14
+ return <SegmentedPicker options={items} valueKey="key" value={activeKey} onChange={onChange} {...props} />
15
+ }
@@ -0,0 +1,3 @@
1
+ export * from './TabsHandler'
2
+ export * from './TabsMenu'
3
+ export * from './ActiveTabContent'
@@ -1,4 +1,19 @@
1
- import { adjust, compose, defaultTo, head, join, juxt, map, pipe, replace, split, tail, toLower, toUpper } from 'ramda'
1
+ import {
2
+ adjust,
3
+ compose,
4
+ defaultTo,
5
+ head,
6
+ join,
7
+ juxt,
8
+ map,
9
+ pipe,
10
+ replace,
11
+ split,
12
+ tail,
13
+ toLower,
14
+ toUpper,
15
+ trim,
16
+ } from 'ramda'
2
17
 
3
18
  export const removeSpecialChars = replace(/[^a-zA-Z ,0-9]/g, '')
4
19
 
@@ -55,3 +70,5 @@ export const truncate = truncateString
55
70
  export const slugify = (text) => {
56
71
  return toSnakeCase(text)
57
72
  }
73
+
74
+ export const normalizeString = pipe(toLower, trim, (str) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''))
@@ -0,0 +1,97 @@
1
+ export class I18n {
2
+ constructor(options = {}) {
3
+ this.language = options.language || 'en'
4
+ this.fallback = options.fallback || 'en'
5
+ this.onChangeLanguage = options.onChangeLanguage
6
+ this.resources = options.resources || {}
7
+ this.loader = options.loader
8
+ }
9
+
10
+ addResources(lang, ns, data) {
11
+ if (!this.resources[lang]) this.resources[lang] = {}
12
+ this.resources[lang][ns] = { ...this.resources[lang][ns], ...data }
13
+ }
14
+
15
+ changeLanguage(lang) {
16
+ this.language = lang
17
+ this.onChangeLanguage?.(lang)
18
+ }
19
+
20
+ async tAsync(key, opts = {}) {
21
+ const { ns = 'common' } = opts
22
+ if (!this.resources[this.language]?.[ns] && this.loader) {
23
+ await this._loadNamespace(this.language, ns)
24
+ }
25
+ return this.t(key, opts)
26
+ }
27
+
28
+ t(key, opts = {}) {
29
+ let { ns = 'common', context, ...vars } = opts
30
+ const count = vars?.count
31
+ if (key.includes(':')) {
32
+ const splittedKey = key.split(':')
33
+ ns = splittedKey[0]
34
+ key = splittedKey[1]
35
+ }
36
+ const langData = this.resources[this.language]?.[ns]
37
+ let value = this._resolveKey(langData, key, count, context)
38
+ if (context) console.log(context, value)
39
+
40
+ if (!value) {
41
+ const fallbackData = this.resources[this.fallback]?.[ns]
42
+ value = this._resolveKey(fallbackData, key, count, context) || key
43
+ }
44
+
45
+ return this._interpolate(value, vars)
46
+ }
47
+
48
+ async _loadNamespace(lang, ns) {
49
+ if (!this.loader) return
50
+ const data = await this.loader(lang, ns)
51
+ this.addResources(lang, ns, data)
52
+ }
53
+
54
+ _resolveKey(nsData, key, count, context) {
55
+ if (!nsData) return null
56
+
57
+ // Support for nested keys like "a.b.c"
58
+ const parts = key.split('.')
59
+ const basePath = parts.slice(0, -1)
60
+ const baseKey = parts[parts.length - 1]
61
+ const parent = basePath.reduce((acc, part) => (acc ? acc[part] : undefined), nsData) || nsData
62
+
63
+ if (!parent) return null
64
+
65
+ // Support for context (e.g., gender) - tries key_context
66
+ if (context) {
67
+ const contextKey = `${baseKey}_${context}`
68
+ if (parent[contextKey] != null) {
69
+ return parent[contextKey]
70
+ }
71
+ }
72
+
73
+ // Support for pluralization - tries key_one for singular
74
+ if (typeof count === 'number') {
75
+ if (count === 0) {
76
+ const singularKey = `${baseKey}_zero`
77
+ if (parent[singularKey] != null) {
78
+ return parent[singularKey]
79
+ }
80
+ }
81
+ if (count === 1) {
82
+ const singularKey = `${baseKey}_one`
83
+ if (parent[singularKey] != null) {
84
+ return parent[singularKey]
85
+ }
86
+ }
87
+ }
88
+
89
+ // Return the base key
90
+ return parent[baseKey]
91
+ }
92
+
93
+ _interpolate(str, vars) {
94
+ if (typeof str !== 'string') return str
95
+ return str.replace(/\{\{(.*?)\}\}/g, (_, v) => vars[v.trim()] ?? '')
96
+ }
97
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react'
2
+
3
+ import { I18n } from './I18n'
4
+
5
+ const I18nContext = React.createContext()
6
+
7
+ export function I18nProvider({ children, i18n, config }) {
8
+ const i18nRef = React.useRef(i18n || new I18n(config))
9
+ const [v, setVersion] = React.useState(0)
10
+
11
+ const forceUpdate = React.useCallback(() => setVersion((v) => v + 1), [])
12
+
13
+ const changeLanguage = React.useCallback(
14
+ (lang) => {
15
+ i18nRef.current.changeLanguage(lang)
16
+ forceUpdate()
17
+ },
18
+ [forceUpdate]
19
+ )
20
+
21
+ const value = React.useMemo(
22
+ () => ({
23
+ t: (...args) => i18nRef.current.t(...args),
24
+ i18n: i18nRef.current,
25
+ v,
26
+ language: i18nRef.current.language,
27
+ changeLanguage,
28
+ }),
29
+ [v, forceUpdate, changeLanguage]
30
+ )
31
+
32
+ return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
33
+ }
34
+
35
+ export function useTranslation(namespace) {
36
+ const ctx = React.useContext(I18nContext)
37
+ if (!ctx) throw new Error('useTranslation must be used inside <I18nProvider>')
38
+ const t = (key, opts = {}) => ctx?.t?.(key, { ns: namespace, ...opts })
39
+ return { ...ctx, t }
40
+ }
@@ -0,0 +1,2 @@
1
+ export * from './I18nProvider'
2
+ export * from './I18n'