@helpwave/hightide-native 0.0.1 → 0.0.5

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 (89) hide show
  1. package/README.md +56 -0
  2. package/package.json +3 -3
  3. package/src/components/chat/ChatAttachmentCard.tsx +110 -0
  4. package/src/components/chat/ChatConversationList.tsx +72 -0
  5. package/src/components/chat/ChatConversationRow.tsx +153 -0
  6. package/src/components/chat/ChatDateDivider.tsx +55 -0
  7. package/src/components/chat/ChatMessageBubble.tsx +112 -0
  8. package/src/components/chat/ChatMessageCard.tsx +118 -0
  9. package/src/components/chat/ChatMessageComposer.tsx +115 -0
  10. package/src/components/chat/ChatMessageList.tsx +66 -0
  11. package/src/components/chat/ChatQuickReplyChip.tsx +72 -0
  12. package/src/components/chat/ChatSystemLine.tsx +68 -0
  13. package/src/components/chat/ChatThreadHeader.tsx +90 -0
  14. package/src/components/chat/index.ts +11 -0
  15. package/src/components/index.ts +4 -7
  16. package/src/components/menu/Menu.tsx +61 -0
  17. package/src/components/menu/MenuActionItem.tsx +90 -0
  18. package/src/components/menu/MenuItem.tsx +73 -0
  19. package/src/components/menu/MenuNavigationItem.tsx +90 -0
  20. package/src/components/menu/index.ts +4 -0
  21. package/src/components/user-interaction/Button.tsx +101 -0
  22. package/src/components/{Checkbox → user-interaction}/Checkbox.tsx +38 -16
  23. package/src/components/user-interaction/IconButton.tsx +95 -0
  24. package/src/components/{Input → user-interaction}/Input.tsx +35 -14
  25. package/src/components/{MultiSelect → user-interaction}/MultiSelect.tsx +47 -15
  26. package/src/components/{Select → user-interaction}/Select.tsx +44 -12
  27. package/src/components/user-interaction/index.ts +6 -0
  28. package/src/components/{Chip → visualization-and-display}/Chip.tsx +36 -20
  29. package/src/components/visualization-and-display/Icon.tsx +27 -0
  30. package/src/components/{Chip → visualization-and-display}/index.ts +1 -0
  31. package/src/global-contexts/HightideContext.ts +4 -1
  32. package/src/global-contexts/HightideProvider.tsx +22 -10
  33. package/src/global-contexts/hightide-config/HightideConfigUtils.ts +15 -12
  34. package/src/global-contexts/localization/LocalizationProvider.tsx +4 -2
  35. package/src/global-contexts/localization/forward-exports.ts +1 -0
  36. package/src/global-contexts/theme/ThemeContext.ts +11 -6
  37. package/src/global-contexts/theme/ThemeProvider.tsx +35 -38
  38. package/src/hooks/useMultiSelect.ts +1 -0
  39. package/src/hooks/useNativeKeyValueStore.ts +7 -1
  40. package/src/hooks/useSelect.ts +1 -0
  41. package/src/theme/resolvers/button.ts +38 -23
  42. package/src/theme/resolvers/chat.ts +418 -0
  43. package/src/theme/resolvers/checkbox.ts +26 -21
  44. package/src/theme/resolvers/chip.ts +32 -22
  45. package/src/theme/resolvers/coloring.ts +21 -17
  46. package/src/theme/resolvers/iconButton.ts +28 -18
  47. package/src/theme/resolvers/index.ts +2 -0
  48. package/src/theme/resolvers/input.ts +28 -19
  49. package/src/theme/resolvers/menu.ts +120 -0
  50. package/src/theme/resolvers/multiSelect.ts +39 -34
  51. package/src/theme/resolvers/select.ts +35 -30
  52. package/src/theme/themes/createTheme.ts +44 -15
  53. package/src/theme/themes/nativeThemes.ts +7 -6
  54. package/src/theme/types/color.ts +60 -0
  55. package/src/theme/types/components/button.ts +30 -0
  56. package/src/theme/types/components/chat.ts +198 -0
  57. package/src/theme/types/components/checkbox.ts +33 -0
  58. package/src/theme/types/components/chip.ts +30 -0
  59. package/src/theme/types/components/hightide.ts +42 -0
  60. package/src/theme/types/components/iconButton.ts +30 -0
  61. package/src/theme/types/components/index.ts +10 -0
  62. package/src/theme/types/components/input.ts +19 -0
  63. package/src/theme/types/components/menu.ts +57 -0
  64. package/src/theme/types/components/multiSelect.ts +47 -0
  65. package/src/theme/types/components/select.ts +47 -0
  66. package/src/theme/types/decoration.ts +11 -0
  67. package/src/theme/types/index.ts +4 -7
  68. package/src/theme/types/layout.ts +46 -0
  69. package/src/theme/types/resolver.ts +43 -1
  70. package/src/theme/types/theme.ts +14 -10
  71. package/src/theme/types/typography.ts +18 -0
  72. package/src/components/Button/Button.tsx +0 -101
  73. package/src/components/Button/index.ts +0 -1
  74. package/src/components/Checkbox/index.ts +0 -1
  75. package/src/components/IconButton/IconButton.tsx +0 -89
  76. package/src/components/IconButton/index.ts +0 -1
  77. package/src/components/Input/index.ts +0 -1
  78. package/src/components/MultiSelect/index.ts +0 -1
  79. package/src/components/Select/index.ts +0 -1
  80. package/src/icons/Icon.tsx +0 -35
  81. package/src/icons/index.ts +0 -1
  82. package/src/theme/types/button.ts +0 -41
  83. package/src/theme/types/checkbox.ts +0 -36
  84. package/src/theme/types/chip.ts +0 -41
  85. package/src/theme/types/components.ts +0 -17
  86. package/src/theme/types/iconButton.ts +0 -34
  87. package/src/theme/types/input.ts +0 -25
  88. package/src/theme/types/multiSelect.ts +0 -65
  89. package/src/theme/types/select.ts +0 -77
@@ -0,0 +1,115 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode
4
+ } from 'react'
5
+ import {
6
+ TextInput,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle
11
+ } from 'react-native'
12
+ import { SendHorizontal } from 'lucide-react-native'
13
+
14
+ import { useControlledState } from '@helpwave/hightide-utils/hooks'
15
+
16
+ import { IconButton } from '@/src/components/user-interaction/IconButton'
17
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
18
+ import type {
19
+ ChatMessageComposerInputStyle,
20
+ ChatMessageComposerStyle
21
+ } from '@/src/theme/types/components/chat'
22
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
23
+
24
+ export type ChatMessageComposerProps = Omit<ViewProps, 'style'> & {
25
+ value?: string,
26
+ initialValue?: string,
27
+ onValueChange?: (value: string) => void,
28
+ onSend: (value: string) => void,
29
+ placeholder?: string,
30
+ sendLabel?: string,
31
+ disabled?: boolean,
32
+ actions?: ReactNode,
33
+ trailing?: ReactNode,
34
+ style?: StyleProp<ViewStyle>,
35
+ composerStyle?: StyleOverwrite<Record<string, never>, ChatMessageComposerStyle>,
36
+ inputStyle?: StyleOverwrite<Record<string, never>, ChatMessageComposerInputStyle>,
37
+ }
38
+
39
+ export const ChatMessageComposer = ({
40
+ value: controlledValue,
41
+ initialValue,
42
+ onValueChange,
43
+ onSend,
44
+ placeholder,
45
+ sendLabel = 'Send',
46
+ disabled = false,
47
+ actions,
48
+ trailing,
49
+ style,
50
+ composerStyle,
51
+ inputStyle,
52
+ ...props
53
+ }: ChatMessageComposerProps) => {
54
+ const { theme } = useTheme()
55
+ const [value, setValue] = useControlledState({
56
+ value: controlledValue,
57
+ onValueChange,
58
+ defaultValue: initialValue ?? '',
59
+ })
60
+ const state = useMemo(() => ({}), [])
61
+
62
+ const resolvedComposerStyle = useMemo(
63
+ () => theme.components.chat.messageComposer.container(state, composerStyle),
64
+ [theme, state, composerStyle]
65
+ )
66
+ const resolvedInputStyle = useMemo(
67
+ () => theme.components.chat.messageComposer.input(state, inputStyle),
68
+ [theme, state, inputStyle]
69
+ )
70
+ const placeholderColor = useMemo(
71
+ () => theme.components.chat.messageComposer.placeholderColor(state),
72
+ [theme, state]
73
+ )
74
+
75
+ const send = () => {
76
+ const trimmed = (value ?? '').trim()
77
+ if (!trimmed || disabled) {
78
+ return
79
+ }
80
+ onSend(trimmed)
81
+ setValue('')
82
+ }
83
+
84
+ return (
85
+ <View {...props} style={[resolvedComposerStyle, style]}>
86
+ {actions != null && (
87
+ <View style={{ flexDirection: 'row', alignItems: 'flex-start' }}>
88
+ {actions}
89
+ </View>
90
+ )}
91
+ <TextInput
92
+ value={value ?? ''}
93
+ onChangeText={setValue}
94
+ placeholder={placeholder}
95
+ placeholderTextColor={placeholderColor}
96
+ editable={!disabled}
97
+ multiline
98
+ style={resolvedInputStyle}
99
+ onSubmitEditing={send}
100
+ returnKeyType="send"
101
+ />
102
+ {trailing}
103
+ <IconButton
104
+ accessibilityLabel={sendLabel}
105
+ color="primary"
106
+ coloringStyle="solid"
107
+ disabled={disabled || !(value ?? '').trim()}
108
+ size="md"
109
+ onPress={send}
110
+ icon={SendHorizontal}
111
+ buttonStyle={(prev) => ({ ...prev, borderRadius: 999 })}
112
+ />
113
+ </View>
114
+ )
115
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ useEffect,
3
+ useMemo,
4
+ useRef,
5
+ type ReactNode
6
+ } from 'react'
7
+ import {
8
+ ScrollView,
9
+ type NativeScrollEvent,
10
+ type NativeSyntheticEvent,
11
+ type StyleProp,
12
+ type ViewStyle
13
+ } from 'react-native'
14
+
15
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
16
+ import type { ChatMessageListStyle } from '@/src/theme/types/components/chat'
17
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
18
+
19
+ export type ChatMessageListProps = {
20
+ autoScroll?: boolean,
21
+ children?: ReactNode,
22
+ style?: StyleProp<ViewStyle>,
23
+ listStyle?: StyleOverwrite<Record<string, never>, ChatMessageListStyle>,
24
+ contentContainerStyle?: StyleProp<ViewStyle>,
25
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void,
26
+ }
27
+
28
+ export const ChatMessageList = ({
29
+ autoScroll = true,
30
+ children,
31
+ style,
32
+ listStyle,
33
+ contentContainerStyle,
34
+ onScroll,
35
+ }: ChatMessageListProps) => {
36
+ const { theme } = useTheme()
37
+ const scrollRef = useRef<ScrollView>(null)
38
+ const state = useMemo(() => ({}), [])
39
+
40
+ const resolvedListStyle = useMemo(
41
+ () => theme.components.chat.messageList.container(state, listStyle),
42
+ [theme, state, listStyle]
43
+ )
44
+ const listGap = useMemo(
45
+ () => (theme.components.chat.messageList.container(state) as ViewStyle).gap,
46
+ [theme, state]
47
+ )
48
+
49
+ useEffect(() => {
50
+ if (autoScroll) {
51
+ scrollRef.current?.scrollToEnd({ animated: true })
52
+ }
53
+ }, [autoScroll, children])
54
+
55
+ return (
56
+ <ScrollView
57
+ ref={scrollRef}
58
+ style={[resolvedListStyle, style]}
59
+ contentContainerStyle={[{ gap: listGap }, contentContainerStyle]}
60
+ onScroll={onScroll}
61
+ scrollEventThrottle={16}
62
+ >
63
+ {children}
64
+ </ScrollView>
65
+ )
66
+ }
@@ -0,0 +1,72 @@
1
+ import type { ReactNode } from 'react'
2
+ import {
3
+ Pressable,
4
+ Text,
5
+ type PressableProps,
6
+ type StyleProp,
7
+ type ViewStyle
8
+ } from 'react-native'
9
+
10
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
11
+ import type {
12
+ ChatQuickReplyChipState,
13
+ ChatQuickReplyChipStyle,
14
+ ChatQuickReplyChipTextStyle
15
+ } from '@/src/theme/types/components/chat'
16
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
17
+
18
+ export type ChatQuickReplyChipProps = Omit<PressableProps, 'children' | 'style'> & {
19
+ isActive?: boolean,
20
+ children?: ReactNode,
21
+ style?: StyleProp<ViewStyle>,
22
+ chipStyle?: StyleOverwrite<ChatQuickReplyChipState, ChatQuickReplyChipStyle>,
23
+ textStyle?: StyleOverwrite<ChatQuickReplyChipState, ChatQuickReplyChipTextStyle>,
24
+ }
25
+
26
+ type PressableInteraction = {
27
+ pressed: boolean,
28
+ hovered?: boolean,
29
+ focused?: boolean,
30
+ }
31
+
32
+ export const ChatQuickReplyChip = ({
33
+ isActive = false,
34
+ children,
35
+ disabled,
36
+ style,
37
+ chipStyle,
38
+ textStyle,
39
+ ...props
40
+ }: ChatQuickReplyChipProps) => {
41
+ const { theme } = useTheme()
42
+
43
+ const resolveState = (interaction: PressableInteraction): ChatQuickReplyChipState => ({
44
+ isActive,
45
+ isDisabled: !!disabled,
46
+ isPressed: interaction.pressed,
47
+ isHovered: !!interaction.hovered,
48
+ isFocused: !!interaction.focused,
49
+ })
50
+
51
+ return (
52
+ <Pressable
53
+ {...props}
54
+ disabled={disabled}
55
+ style={(pressableState) => {
56
+ const state = resolveState(pressableState as PressableInteraction)
57
+ return [theme.components.chat.quickReplyChip.container(state, chipStyle), style]
58
+ }}
59
+ >
60
+ {(pressableState) => {
61
+ const state = resolveState(pressableState as PressableInteraction)
62
+ const resolvedText = theme.components.chat.quickReplyChip.text(state, textStyle)
63
+
64
+ if (typeof children === 'string' || typeof children === 'number') {
65
+ return <Text style={resolvedText}>{children}</Text>
66
+ }
67
+
68
+ return children
69
+ }}
70
+ </Pressable>
71
+ )
72
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode
4
+ } from 'react'
5
+ import {
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle
11
+ } from 'react-native'
12
+ import { CheckCheck } from 'lucide-react-native'
13
+
14
+ import type { ColoringType } from '@helpwave/hightide-design/helpers'
15
+
16
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
17
+ import type {
18
+ ChatSystemLineState,
19
+ ChatSystemLineStyle,
20
+ ChatSystemLineTextStyle
21
+ } from '@/src/theme/types/components/chat'
22
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
23
+
24
+ export type ChatSystemLineProps = Omit<ViewProps, 'children' | 'style'> & {
25
+ icon?: ReactNode,
26
+ color?: ColoringType,
27
+ children?: ReactNode,
28
+ style?: StyleProp<ViewStyle>,
29
+ lineStyle?: StyleOverwrite<ChatSystemLineState, ChatSystemLineStyle>,
30
+ textStyle?: StyleOverwrite<ChatSystemLineState, ChatSystemLineTextStyle>,
31
+ }
32
+
33
+ export const ChatSystemLine = ({
34
+ icon,
35
+ color = 'primary',
36
+ children,
37
+ style,
38
+ lineStyle,
39
+ textStyle,
40
+ ...props
41
+ }: ChatSystemLineProps) => {
42
+ const { theme } = useTheme()
43
+ const state = useMemo(() => ({ color }), [color])
44
+
45
+ const resolvedLineStyle = useMemo(
46
+ () => theme.components.chat.systemLine.container(state, lineStyle),
47
+ [theme, state, lineStyle]
48
+ )
49
+ const resolvedTextStyle = useMemo(
50
+ () => theme.components.chat.systemLine.text(state, textStyle),
51
+ [theme, state, textStyle]
52
+ )
53
+ const resolvedIcon = useMemo(
54
+ () => theme.components.chat.systemLine.icon(state),
55
+ [theme, state]
56
+ )
57
+
58
+ return (
59
+ <View {...props} style={[resolvedLineStyle, style]}>
60
+ {icon ?? <CheckCheck size={14} color={resolvedIcon.color} />}
61
+ {typeof children === 'string' || typeof children === 'number' ? (
62
+ <Text style={resolvedTextStyle}>{children}</Text>
63
+ ) : (
64
+ children
65
+ )}
66
+ </View>
67
+ )
68
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode
4
+ } from 'react'
5
+ import {
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle
11
+ } from 'react-native'
12
+
13
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
14
+ import type {
15
+ ChatThreadHeaderStyle,
16
+ ChatThreadHeaderSubtitleStyle,
17
+ ChatThreadHeaderTitleStyle
18
+ } from '@/src/theme/types/components/chat'
19
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
20
+
21
+ export type ChatThreadHeaderProps = Omit<ViewProps, 'style'> & {
22
+ avatar?: ReactNode,
23
+ title: ReactNode,
24
+ subtitle?: ReactNode,
25
+ leftActions?: ReactNode,
26
+ rightActions?: ReactNode,
27
+ style?: StyleProp<ViewStyle>,
28
+ headerStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderStyle>,
29
+ titleStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderTitleStyle>,
30
+ subtitleStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderSubtitleStyle>,
31
+ }
32
+
33
+ export const ChatThreadHeader = ({
34
+ avatar,
35
+ title,
36
+ subtitle,
37
+ leftActions,
38
+ rightActions,
39
+ style,
40
+ headerStyle,
41
+ titleStyle,
42
+ subtitleStyle,
43
+ ...props
44
+ }: ChatThreadHeaderProps) => {
45
+ const { theme } = useTheme()
46
+ const state = useMemo(() => ({}), [])
47
+
48
+ const resolvedHeaderStyle = useMemo(
49
+ () => theme.components.chat.threadHeader.container(state, headerStyle),
50
+ [theme, state, headerStyle]
51
+ )
52
+ const resolvedTitleStyle = useMemo(
53
+ () => theme.components.chat.threadHeader.title(state, titleStyle),
54
+ [theme, state, titleStyle]
55
+ )
56
+ const resolvedSubtitleStyle = useMemo(
57
+ () => theme.components.chat.threadHeader.subtitle(state, subtitleStyle),
58
+ [theme, state, subtitleStyle]
59
+ )
60
+
61
+ return (
62
+ <View {...props} style={[resolvedHeaderStyle, style]}>
63
+ {leftActions != null && (
64
+ <View style={{ flexDirection: 'row', alignItems: 'center', flexShrink: 0 }}>
65
+ {leftActions}
66
+ </View>
67
+ )}
68
+ {avatar}
69
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
70
+ {typeof title === 'string' || typeof title === 'number' ? (
71
+ <Text style={resolvedTitleStyle} numberOfLines={1}>{title}</Text>
72
+ ) : (
73
+ title
74
+ )}
75
+ {subtitle != null && (
76
+ typeof subtitle === 'string' || typeof subtitle === 'number' ? (
77
+ <Text style={resolvedSubtitleStyle} numberOfLines={1}>{subtitle}</Text>
78
+ ) : (
79
+ subtitle
80
+ )
81
+ )}
82
+ </View>
83
+ {rightActions != null && (
84
+ <View style={{ flexDirection: 'row', alignItems: 'center', flexShrink: 0 }}>
85
+ {rightActions}
86
+ </View>
87
+ )}
88
+ </View>
89
+ )
90
+ }
@@ -0,0 +1,11 @@
1
+ export * from './ChatAttachmentCard'
2
+ export * from './ChatConversationList'
3
+ export * from './ChatConversationRow'
4
+ export * from './ChatDateDivider'
5
+ export * from './ChatMessageBubble'
6
+ export * from './ChatMessageCard'
7
+ export * from './ChatMessageComposer'
8
+ export * from './ChatMessageList'
9
+ export * from './ChatQuickReplyChip'
10
+ export * from './ChatSystemLine'
11
+ export * from './ChatThreadHeader'
@@ -1,7 +1,4 @@
1
- export * from './Button'
2
- export * from './Checkbox'
3
- export * from './Chip'
4
- export * from './IconButton'
5
- export * from './Input'
6
- export * from './MultiSelect'
7
- export * from './Select'
1
+ export * from './chat'
2
+ export * from './menu'
3
+ export * from './user-interaction'
4
+ export * from './visualization-and-display'
@@ -0,0 +1,61 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode
4
+ } from 'react'
5
+ import {
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle
11
+ } from 'react-native'
12
+
13
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
14
+ import type {
15
+ MenuCardStyle,
16
+ MenuSectionStyle,
17
+ MenuSectionTitleStyle
18
+ } from '@/src/theme/types/components/menu'
19
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
20
+
21
+ export type MenuProps = Omit<ViewProps, 'children' | 'style'> & {
22
+ title: string,
23
+ children?: ReactNode,
24
+ style?: StyleProp<ViewStyle>,
25
+ sectionStyle?: StyleOverwrite<Record<string, never>, MenuSectionStyle>,
26
+ titleStyle?: StyleOverwrite<Record<string, never>, MenuSectionTitleStyle>,
27
+ cardStyle?: StyleOverwrite<Record<string, never>, MenuCardStyle>,
28
+ }
29
+
30
+ export const Menu = ({
31
+ title,
32
+ children,
33
+ style,
34
+ sectionStyle,
35
+ titleStyle,
36
+ cardStyle,
37
+ ...props
38
+ }: MenuProps) => {
39
+ const { theme } = useTheme()
40
+ const state = useMemo(() => ({}), [])
41
+
42
+ const resolvedSectionStyle = useMemo(
43
+ () => theme.components.menu.section(state, sectionStyle),
44
+ [theme, state, sectionStyle]
45
+ )
46
+ const resolvedTitleStyle = useMemo(
47
+ () => theme.components.menu.sectionTitle(state, titleStyle),
48
+ [theme, state, titleStyle]
49
+ )
50
+ const resolvedCardStyle = useMemo(
51
+ () => theme.components.menu.card(state, cardStyle),
52
+ [theme, state, cardStyle]
53
+ )
54
+
55
+ return (
56
+ <View {...props} style={[resolvedSectionStyle, style]}>
57
+ <Text style={resolvedTitleStyle}>{title}</Text>
58
+ <View style={resolvedCardStyle}>{children}</View>
59
+ </View>
60
+ )
61
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ Fragment,
3
+ useMemo,
4
+ type ReactNode
5
+ } from 'react'
6
+ import {
7
+ Pressable,
8
+ Text,
9
+ View,
10
+ type PressableProps,
11
+ type StyleProp,
12
+ type ViewStyle
13
+ } from 'react-native'
14
+
15
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
16
+ import type {
17
+ MenuActionItemLabelStyle,
18
+ MenuActionItemState,
19
+ MenuActionItemStyle
20
+ } from '@/src/theme/types/components/menu'
21
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
22
+
23
+ export type MenuActionItemProps = Omit<PressableProps, 'children' | 'style'> & {
24
+ label: string,
25
+ leading?: ReactNode,
26
+ trailing?: ReactNode,
27
+ danger?: boolean,
28
+ style?: StyleProp<ViewStyle>,
29
+ itemStyle?: StyleOverwrite<MenuActionItemState, MenuActionItemStyle>,
30
+ labelStyle?: StyleOverwrite<MenuActionItemState, MenuActionItemLabelStyle>,
31
+ }
32
+
33
+ type PressableInteraction = {
34
+ pressed: boolean,
35
+ hovered?: boolean,
36
+ focused?: boolean,
37
+ }
38
+
39
+ export const MenuActionItem = ({
40
+ label,
41
+ leading,
42
+ trailing,
43
+ danger = false,
44
+ disabled,
45
+ style,
46
+ itemStyle,
47
+ labelStyle,
48
+ ...props
49
+ }: MenuActionItemProps) => {
50
+ const { theme } = useTheme()
51
+
52
+ const resolveState = (interaction: PressableInteraction): MenuActionItemState => ({
53
+ isDanger: danger,
54
+ isDisabled: !!disabled,
55
+ isPressed: interaction.pressed,
56
+ isHovered: !!interaction.hovered,
57
+ isFocused: !!interaction.focused,
58
+ })
59
+
60
+ const resolvedContentStyle = useMemo(
61
+ () => theme.components.menu.actionItemContent({}),
62
+ [theme]
63
+ )
64
+
65
+ return (
66
+ <Pressable
67
+ {...props}
68
+ disabled={disabled}
69
+ style={(pressableState) => {
70
+ const state = resolveState(pressableState as PressableInteraction)
71
+ return [theme.components.menu.actionItem(state, itemStyle), style]
72
+ }}
73
+ >
74
+ {(pressableState) => {
75
+ const state = resolveState(pressableState as PressableInteraction)
76
+ const resolvedLabelStyle = theme.components.menu.actionItemLabel(state, labelStyle)
77
+
78
+ return (
79
+ <Fragment>
80
+ {leading}
81
+ <View style={resolvedContentStyle}>
82
+ <Text style={resolvedLabelStyle}>{label}</Text>
83
+ </View>
84
+ {trailing}
85
+ </Fragment>
86
+ )
87
+ }}
88
+ </Pressable>
89
+ )
90
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode
4
+ } from 'react'
5
+ import {
6
+ Text,
7
+ View,
8
+ type StyleProp,
9
+ type ViewProps,
10
+ type ViewStyle
11
+ } from 'react-native'
12
+
13
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
14
+ import type {
15
+ MenuItemLabelStyle,
16
+ MenuItemStyle,
17
+ MenuItemValueStyle
18
+ } from '@/src/theme/types/components/menu'
19
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
20
+
21
+ export type MenuItemProps = Omit<ViewProps, 'style'> & {
22
+ label: string,
23
+ value: string,
24
+ leading?: ReactNode,
25
+ trailing?: ReactNode,
26
+ style?: StyleProp<ViewStyle>,
27
+ itemStyle?: StyleOverwrite<Record<string, never>, MenuItemStyle>,
28
+ labelStyle?: StyleOverwrite<Record<string, never>, MenuItemLabelStyle>,
29
+ valueStyle?: StyleOverwrite<Record<string, never>, MenuItemValueStyle>,
30
+ }
31
+
32
+ export const MenuItem = ({
33
+ label,
34
+ value,
35
+ leading,
36
+ trailing,
37
+ style,
38
+ itemStyle,
39
+ labelStyle,
40
+ valueStyle,
41
+ ...props
42
+ }: MenuItemProps) => {
43
+ const { theme } = useTheme()
44
+ const state = useMemo(() => ({}), [])
45
+
46
+ const resolvedItemStyle = useMemo(
47
+ () => theme.components.menu.item(state, itemStyle),
48
+ [theme, state, itemStyle]
49
+ )
50
+ const resolvedContentStyle = useMemo(
51
+ () => theme.components.menu.itemContent(state),
52
+ [theme, state]
53
+ )
54
+ const resolvedLabelStyle = useMemo(
55
+ () => theme.components.menu.itemLabel(state, labelStyle),
56
+ [theme, state, labelStyle]
57
+ )
58
+ const resolvedValueStyle = useMemo(
59
+ () => theme.components.menu.itemValue(state, valueStyle),
60
+ [theme, state, valueStyle]
61
+ )
62
+
63
+ return (
64
+ <View {...props} style={[resolvedItemStyle, style]}>
65
+ {leading}
66
+ <View style={resolvedContentStyle}>
67
+ <Text style={resolvedLabelStyle}>{label}</Text>
68
+ <Text style={resolvedValueStyle}>{value}</Text>
69
+ </View>
70
+ {trailing}
71
+ </View>
72
+ )
73
+ }