@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.
- package/dist/DynamicStyleTag.js +5 -0
- package/dist/DynamicStyleTag.native.js +1 -0
- package/dist/NekoUI.js +1 -1
- package/dist/abstractions/AnimatedView.web.js +1 -0
- package/dist/abstractions/FlatList.js +1 -1
- package/dist/abstractions/FlatList.native.js +1 -1
- package/dist/abstractions/StaticList.js +1 -0
- package/dist/abstractions/helpers/useSafeAreaInsets.js +1 -0
- package/dist/abstractions/helpers/useSafeAreaInsets.native.js +1 -0
- package/dist/components/actions/Button.js +1 -1
- package/dist/components/actions/Dropdown.js +1 -1
- package/dist/components/actions/FloatingButton.js +1 -0
- package/dist/components/actions/index.js +1 -1
- package/dist/components/actions/menu/VerticalMenu.js +1 -1
- package/dist/components/calendar/_helpers/calendarDays.js +1 -1
- package/dist/components/feedback/alerter.js +1 -1
- package/dist/components/feedback/confirmer.js +1 -1
- package/dist/components/helpers/ConditionalLazyRender.js +1 -0
- package/dist/components/helpers/LazyAction.js +1 -0
- package/dist/components/helpers/LazyRender.js +1 -1
- package/dist/components/helpers/LazyRender.native.js +1 -1
- package/dist/components/helpers/index.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/inputs/DateInput.js +1 -1
- package/dist/components/inputs/InputWrapper.js +1 -1
- package/dist/components/inputs/LinkInput.js +1 -1
- package/dist/components/inputs/NumberInput.js +1 -0
- package/dist/components/inputs/Picker.js +1 -1
- package/dist/components/inputs/Radio.js +1 -1
- package/dist/components/inputs/RateInput.js +1 -0
- package/dist/components/inputs/SegmentedPicker.js +1 -0
- package/dist/components/inputs/Select.js +1 -0
- package/dist/components/inputs/datePicker/DayPicker.js +1 -1
- package/dist/components/inputs/datePicker/MonthPicker.js +1 -1
- package/dist/components/inputs/datePicker/QuarterPicker.js +1 -1
- package/dist/components/inputs/datePicker/WeekPicker.js +1 -1
- package/dist/components/inputs/datePicker/YearPicker.js +1 -1
- package/dist/components/inputs/index.js +1 -1
- package/dist/components/list/FlatList.js +1 -1
- package/dist/components/presentation/Rate.js +1 -0
- package/dist/components/presentation/RateTag.js +1 -0
- package/dist/components/presentation/Result.js +1 -1
- package/dist/components/presentation/Tooltip.js +1 -1
- package/dist/components/presentation/index.js +1 -1
- package/dist/components/structure/Accordion.js +1 -1
- package/dist/components/structure/Row.js +1 -1
- package/dist/components/structure/Segment.js +1 -0
- package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +1 -1
- package/dist/components/structure/bottomDrawer/native/utils.js +1 -1
- package/dist/components/structure/bottomDrawer/web/BottomDrawer.js +1 -1
- package/dist/components/structure/index.js +1 -1
- package/dist/components/structure/overlay/OverlayHandler.js +1 -1
- package/dist/components/structure/popover/Popover.js +1 -1
- package/dist/components/structure/popover/Popover_BU.js +1 -0
- package/dist/components/tabs/ActiveTabContent.js +1 -0
- package/dist/components/tabs/TabsHandler.js +1 -0
- package/dist/components/tabs/TabsMenu.js +1 -0
- package/dist/components/tabs/index.js +1 -0
- package/dist/helpers/string.js +1 -1
- package/dist/i18n/I18n.js +1 -0
- package/dist/i18n/I18nProvider.js +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/index.css +4 -0
- package/dist/index.js +1 -1
- package/dist/modifiers/animations/fadeEffect.web.js +1 -0
- package/dist/modifiers/animations/scrollEffect.web.js +1 -0
- package/dist/modifiers/animations/slideEffect.web.js +1 -0
- package/dist/modifiers/fullColor.js +1 -1
- package/dist/modifiers/overflow.js +1 -1
- package/dist/modifiers/position.js +1 -1
- package/dist/theme/default/base.js +1 -1
- package/package.json +1 -1
- package/src/DynamicStyleTag.js +21 -0
- package/src/DynamicStyleTag.native.js +3 -0
- package/src/NekoUI.js +12 -7
- package/src/abstractions/AnimatedView.web.js +3 -0
- package/src/abstractions/FlatList.js +2 -38
- package/src/abstractions/FlatList.native.js +8 -4
- package/src/abstractions/StaticList.js +51 -0
- package/src/abstractions/helpers/useSafeAreaInsets.js +3 -0
- package/src/abstractions/helpers/useSafeAreaInsets.native.js +3 -0
- package/src/components/actions/Button.js +15 -13
- package/src/components/actions/Dropdown.js +13 -9
- package/src/components/actions/FloatingButton.js +87 -0
- package/src/components/actions/index.js +1 -0
- package/src/components/actions/menu/VerticalMenu.js +29 -4
- package/src/components/calendar/_helpers/calendarDays.js +2 -0
- package/src/components/feedback/alerter.js +1 -1
- package/src/components/feedback/confirmer.js +2 -2
- package/src/components/helpers/ConditionalLazyRender.js +6 -0
- package/src/components/helpers/LazyAction.js +22 -0
- package/src/components/helpers/LazyRender.js +2 -2
- package/src/components/helpers/LazyRender.native.js +1 -1
- package/src/components/helpers/index.js +1 -0
- package/src/components/index.js +1 -0
- package/src/components/inputs/DateInput.js +11 -1
- package/src/components/inputs/InputWrapper.js +0 -1
- package/src/components/inputs/LinkInput.js +3 -3
- package/src/components/inputs/NumberInput.js +105 -0
- package/src/components/inputs/Picker.js +61 -9
- package/src/components/inputs/Radio.js +1 -1
- package/src/components/inputs/RateInput.js +62 -0
- package/src/components/inputs/SegmentedPicker.js +62 -0
- package/src/components/inputs/Select.js +189 -0
- package/src/components/inputs/datePicker/DayPicker.js +4 -5
- package/src/components/inputs/datePicker/MonthPicker.js +2 -2
- package/src/components/inputs/datePicker/QuarterPicker.js +2 -2
- package/src/components/inputs/datePicker/WeekPicker.js +2 -2
- package/src/components/inputs/datePicker/YearPicker.js +9 -6
- package/src/components/inputs/index.js +4 -0
- package/src/components/list/FlatList.js +41 -4
- package/src/components/presentation/Rate.js +58 -0
- package/src/components/presentation/RateTag.js +35 -0
- package/src/components/presentation/Result.js +2 -2
- package/src/components/presentation/Tooltip.js +1 -0
- package/src/components/presentation/index.js +2 -0
- package/src/components/structure/Accordion.js +1 -1
- package/src/components/structure/Row.js +9 -1
- package/src/components/structure/Segment.js +51 -0
- package/src/components/structure/bottomDrawer/native/BottomDrawer.js +4 -1
- package/src/components/structure/bottomDrawer/native/utils.js +29 -22
- package/src/components/structure/bottomDrawer/web/BottomDrawer.js +3 -1
- package/src/components/structure/index.js +1 -0
- package/src/components/structure/overlay/OverlayHandler.js +6 -1
- package/src/components/structure/popover/Popover.js +33 -19
- package/src/components/structure/popover/Popover_BU.js +157 -0
- package/src/components/tabs/ActiveTabContent.js +35 -0
- package/src/components/tabs/TabsHandler.js +16 -0
- package/src/components/tabs/TabsMenu.js +15 -0
- package/src/components/tabs/index.js +3 -0
- package/src/helpers/string.js +18 -1
- package/src/i18n/I18n.js +97 -0
- package/src/i18n/I18nProvider.js +40 -0
- package/src/i18n/index.js +2 -0
- package/src/index.css +4 -0
- package/src/index.js +1 -0
- package/src/modifiers/animations/fadeEffect.web.js +3 -0
- package/src/modifiers/animations/scrollEffect.web.js +3 -0
- package/src/modifiers/animations/slideEffect.web.js +3 -0
- package/src/modifiers/fullColor.js +5 -2
- package/src/modifiers/overflow.js +6 -1
- package/src/modifiers/position.js +7 -0
- 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(
|
|
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
|
-
|
|
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(
|
|
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 = (
|
|
3
|
+
export const BottomDrawer = ({ snapPoints, contentProps, ...props }) => {
|
|
4
|
+
return <Drawer height={snapPoints?.[0] || 400} contentProps={{ padding: 0, ...contentProps }} {...props} />
|
|
5
|
+
}
|
|
@@ -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
|
|
32
|
-
|
|
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
|
+
}
|
package/src/helpers/string.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import {
|
|
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, ''))
|
package/src/i18n/I18n.js
ADDED
|
@@ -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
|
+
}
|