@helpwave/hightide-native 0.0.1 → 0.0.2

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