@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
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @helpwave/hightide-native
2
+
3
+ React Native components, theme resolvers, and providers for helpwave apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @helpwave/hightide-native
9
+ ```
10
+
11
+ Peer dependencies (install in your app if not already present):
12
+
13
+ ```bash
14
+ pnpm add react react-native react-native-svg @react-native-async-storage/async-storage
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Wrap your app with `HightideProvider`:
20
+
21
+ ```tsx
22
+ import { HightideProvider } from "@helpwave/hightide-native/global-contexts";
23
+
24
+ <HightideProvider theme={{ theme: "system" }} locale={{ locale: "system" }}>
25
+ {children}
26
+ </HightideProvider>;
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Import from the package subpaths (there is no root export):
32
+
33
+ ```tsx
34
+ import { Button, Input, Select } from "@helpwave/hightide-native/components";
35
+ import { HightideProvider } from "@helpwave/hightide-native/global-contexts";
36
+ import { useSelect } from "@helpwave/hightide-native/hooks";
37
+ import { Icon } from "@helpwave/hightide-native/icons";
38
+ ```
39
+
40
+ | Subpath | Contents |
41
+ | --- | --- |
42
+ | `@helpwave/hightide-native/components` | UI components (`Button`, `Checkbox`, `Chip`, `Input`, …) |
43
+ | `@helpwave/hightide-native/global-contexts` | `HightideProvider` and related context providers |
44
+ | `@helpwave/hightide-native/hooks` | Native hooks (`useSelect`, `useMultiSelect`, …) |
45
+ | `@helpwave/hightide-native/icons` | Icon components |
46
+ | `@helpwave/hightide-native/theme` | Theme factories and style resolvers |
47
+ | `@helpwave/hightide-native/types` | Shared TypeScript types |
48
+
49
+ ## Development
50
+
51
+ From the monorepo root:
52
+
53
+ ```bash
54
+ pnpm --filter @helpwave/hightide-native run init
55
+ pnpm --filter @helpwave/hightide-native run storybook
56
+ ```
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "access": "public"
11
11
  },
12
12
  "license": "AGPL-3.0-only",
13
- "version": "0.0.1",
13
+ "version": "0.0.2",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "src"
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "lucide-react-native": "0.561.0",
46
- "@helpwave/hightide-design": "0.0.1",
47
- "@helpwave/hightide-utils": "0.0.1"
46
+ "@helpwave/hightide-utils": "0.0.1",
47
+ "@helpwave/hightide-design": "0.0.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@react-native-async-storage/async-storage": ">=2.0.0",
@@ -0,0 +1,97 @@
1
+ import { useMemo, type ReactNode } from 'react'
2
+ import { Text, View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native'
3
+ import { Download, FileText } from 'lucide-react-native'
4
+ import { useTheme } from '../../global-contexts/theme'
5
+ import type {
6
+ ChatAttachmentCardMetadataStyle,
7
+ ChatAttachmentCardNameStyle,
8
+ ChatAttachmentCardState,
9
+ ChatAttachmentCardStyle,
10
+ ChatMessageDirection,
11
+ StyleOverwrite
12
+ } from '../../theme'
13
+ import { IconButton } from '../user-interaction'
14
+
15
+ export type ChatAttachmentCardProps = Omit<ViewProps, 'style'> & {
16
+ name: ReactNode,
17
+ metadata?: ReactNode,
18
+ icon?: ReactNode,
19
+ direction?: ChatMessageDirection,
20
+ downloadLabel?: string,
21
+ onDownload?: () => void,
22
+ style?: StyleProp<ViewStyle>,
23
+ cardStyle?: StyleOverwrite<ChatAttachmentCardState, ChatAttachmentCardStyle>,
24
+ nameStyle?: StyleOverwrite<Record<string, never>, ChatAttachmentCardNameStyle>,
25
+ metadataStyle?: StyleOverwrite<Record<string, never>, ChatAttachmentCardMetadataStyle>,
26
+ }
27
+
28
+ export const ChatAttachmentCard = ({
29
+ name,
30
+ metadata,
31
+ icon,
32
+ direction = 'incoming',
33
+ downloadLabel = 'Download',
34
+ onDownload,
35
+ style,
36
+ cardStyle,
37
+ nameStyle,
38
+ metadataStyle,
39
+ ...props
40
+ }: ChatAttachmentCardProps) => {
41
+ const { theme } = useTheme()
42
+ const state = useMemo(() => ({ direction }), [direction])
43
+ const staticState = useMemo(() => ({}), [])
44
+
45
+ const resolvedCardStyle = useMemo(
46
+ () => theme.components.chat.attachmentCard(state, cardStyle),
47
+ [theme, state, cardStyle]
48
+ )
49
+ const resolvedIconStyle = useMemo(
50
+ () => theme.components.chat.attachmentCardIcon(staticState),
51
+ [theme, staticState]
52
+ )
53
+ const resolvedIconColor = useMemo(
54
+ () => theme.components.chat.attachmentCardIconColor(staticState),
55
+ [theme, staticState]
56
+ )
57
+ const resolvedNameStyle = useMemo(
58
+ () => theme.components.chat.attachmentCardName(staticState, nameStyle),
59
+ [theme, staticState, nameStyle]
60
+ )
61
+ const resolvedMetadataStyle = useMemo(
62
+ () => theme.components.chat.attachmentCardMetadata(staticState, metadataStyle),
63
+ [theme, staticState, metadataStyle]
64
+ )
65
+
66
+ return (
67
+ <View {...props} style={[resolvedCardStyle, style]}>
68
+ <View style={resolvedIconStyle}>
69
+ {icon ?? <FileText size={22} color={resolvedIconColor.color} />}
70
+ </View>
71
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
72
+ {typeof name === 'string' || typeof name === 'number' ? (
73
+ <Text style={resolvedNameStyle} numberOfLines={1}>{name}</Text>
74
+ ) : (
75
+ name
76
+ )}
77
+ {metadata != null && (
78
+ typeof metadata === 'string' || typeof metadata === 'number' ? (
79
+ <Text style={resolvedMetadataStyle}>{metadata}</Text>
80
+ ) : (
81
+ metadata
82
+ )
83
+ )}
84
+ </View>
85
+ {onDownload && (
86
+ <IconButton
87
+ accessibilityLabel={downloadLabel}
88
+ size="sm"
89
+ color="primary"
90
+ coloringStyle="text"
91
+ onPress={onDownload}
92
+ icon={Download}
93
+ />
94
+ )}
95
+ </View>
96
+ )
97
+ }
@@ -0,0 +1,68 @@
1
+ import { useMemo, type ReactNode } from 'react'
2
+ import {
3
+ ScrollView,
4
+ View,
5
+ type StyleProp,
6
+ type ViewProps,
7
+ type ViewStyle
8
+ } from 'react-native'
9
+ import { useTheme } from '../../global-contexts/theme'
10
+ import type {
11
+ ChatConversationListFooterStyle,
12
+ ChatConversationListHeaderStyle,
13
+ ChatConversationListStyle,
14
+ StyleOverwrite
15
+ } from '../../theme'
16
+
17
+ export type ChatConversationListProps = Omit<ViewProps, 'children' | 'style'> & {
18
+ header?: ReactNode,
19
+ footer?: ReactNode,
20
+ children?: ReactNode,
21
+ style?: StyleProp<ViewStyle>,
22
+ listStyle?: StyleOverwrite<Record<string, never>, ChatConversationListStyle>,
23
+ headerStyle?: StyleOverwrite<Record<string, never>, ChatConversationListHeaderStyle>,
24
+ contentStyle?: StyleProp<ViewStyle>,
25
+ footerStyle?: StyleOverwrite<Record<string, never>, ChatConversationListFooterStyle>,
26
+ }
27
+
28
+ export const ChatConversationList = ({
29
+ header,
30
+ footer,
31
+ children,
32
+ style,
33
+ listStyle,
34
+ headerStyle,
35
+ contentStyle,
36
+ footerStyle,
37
+ ...props
38
+ }: ChatConversationListProps) => {
39
+ const { theme } = useTheme()
40
+ const state = useMemo(() => ({}), [])
41
+
42
+ const resolvedListStyle = useMemo(
43
+ () => theme.components.chat.conversationList(state, listStyle),
44
+ [theme, state, listStyle]
45
+ )
46
+ const resolvedHeaderStyle = useMemo(
47
+ () => theme.components.chat.conversationListHeader(state, headerStyle),
48
+ [theme, state, headerStyle]
49
+ )
50
+ const resolvedFooterStyle = useMemo(
51
+ () => theme.components.chat.conversationListFooter(state, footerStyle),
52
+ [theme, state, footerStyle]
53
+ )
54
+
55
+ return (
56
+ <View {...props} style={[resolvedListStyle, style]}>
57
+ {header != null && (
58
+ <View style={resolvedHeaderStyle}>{header}</View>
59
+ )}
60
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={contentStyle}>
61
+ {children}
62
+ </ScrollView>
63
+ {footer != null && (
64
+ <View style={resolvedFooterStyle}>{footer}</View>
65
+ )}
66
+ </View>
67
+ )
68
+ }
@@ -0,0 +1,145 @@
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 { Check, CheckCheck } from 'lucide-react-native'
11
+ import { useTheme } from '../../global-contexts/theme'
12
+ import type {
13
+ ChatConversationRowPreviewStyle,
14
+ ChatConversationRowState,
15
+ ChatConversationRowStyle,
16
+ ChatConversationRowTimestampStyle,
17
+ ChatConversationRowTitleStyle,
18
+ StyleOverwrite
19
+ } from '../../theme'
20
+
21
+ export type ChatConversationSentIndicator = 'sent' | 'sentAndReceived'
22
+
23
+ export type ChatConversationRowProps = Omit<PressableProps, 'children' | 'style'> & {
24
+ avatar: ReactNode,
25
+ title: ReactNode,
26
+ timestamp?: ReactNode,
27
+ preview?: ReactNode,
28
+ unreadCount?: number,
29
+ isSelected?: boolean,
30
+ sentIndicator?: ChatConversationSentIndicator,
31
+ style?: StyleProp<ViewStyle>,
32
+ rowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowStyle>,
33
+ titleStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTitleStyle>,
34
+ timestampStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTimestampStyle>,
35
+ previewStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowPreviewStyle>,
36
+ }
37
+
38
+ type PressableInteraction = {
39
+ pressed: boolean,
40
+ hovered?: boolean,
41
+ focused?: boolean,
42
+ }
43
+
44
+ export const ChatConversationRow = ({
45
+ avatar,
46
+ title,
47
+ timestamp,
48
+ preview,
49
+ unreadCount,
50
+ isSelected = false,
51
+ sentIndicator,
52
+ disabled,
53
+ style,
54
+ rowStyle,
55
+ titleStyle,
56
+ timestampStyle,
57
+ previewStyle,
58
+ ...props
59
+ }: ChatConversationRowProps) => {
60
+ const { theme } = useTheme()
61
+ const isUnread = (unreadCount ?? 0) > 0
62
+ const SentIndicatorIcon = sentIndicator === 'sentAndReceived' ? CheckCheck : Check
63
+
64
+ const resolveState = (interaction: PressableInteraction): ChatConversationRowState => ({
65
+ isUnread,
66
+ isSelected,
67
+ isDisabled: !!disabled,
68
+ isPressed: interaction.pressed,
69
+ isHovered: !!interaction.hovered,
70
+ isFocused: !!interaction.focused,
71
+ })
72
+
73
+ const staticState = useMemo(() => ({}), [])
74
+ const unreadBadge = useMemo(
75
+ () => theme.components.chat.conversationRowUnreadBadge(staticState),
76
+ [theme, staticState]
77
+ )
78
+ const unreadBadgeText = useMemo(
79
+ () => theme.components.chat.conversationRowUnreadBadgeText(staticState),
80
+ [theme, staticState]
81
+ )
82
+ const sentIndicatorColor = useMemo(
83
+ () => theme.components.chat.conversationRowSentIndicator(staticState).color,
84
+ [theme, staticState]
85
+ )
86
+
87
+ return (
88
+ <Pressable
89
+ {...props}
90
+ disabled={disabled}
91
+ style={(pressableState) => {
92
+ const state = resolveState(pressableState as PressableInteraction)
93
+ return [theme.components.chat.conversationRow(state, rowStyle), style]
94
+ }}
95
+ >
96
+ {(pressableState) => {
97
+ const state = resolveState(pressableState as PressableInteraction)
98
+ const resolvedTitle = theme.components.chat.conversationRowTitle(state, titleStyle)
99
+ const resolvedTimestamp = theme.components.chat.conversationRowTimestamp(state, timestampStyle)
100
+ const resolvedPreview = theme.components.chat.conversationRowPreview(state, previewStyle)
101
+
102
+ return (
103
+ <Fragment>
104
+ {avatar}
105
+ <View style={{ flex: 1, minWidth: 0, gap: 5 }}>
106
+ <View style={{ flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
107
+ {typeof title === 'string' || typeof title === 'number' ? (
108
+ <Text style={resolvedTitle} numberOfLines={1}>{title}</Text>
109
+ ) : (
110
+ title
111
+ )}
112
+ {timestamp != null && (
113
+ typeof timestamp === 'string' || typeof timestamp === 'number' ? (
114
+ <Text style={resolvedTimestamp}>{timestamp}</Text>
115
+ ) : (
116
+ timestamp
117
+ )
118
+ )}
119
+ </View>
120
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
121
+ <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', gap: 4, minWidth: 0 }}>
122
+ {sentIndicator && (
123
+ <SentIndicatorIcon size={14} color={sentIndicatorColor} />
124
+ )}
125
+ {preview != null && (
126
+ typeof preview === 'string' || typeof preview === 'number' ? (
127
+ <Text style={resolvedPreview} numberOfLines={1}>{preview}</Text>
128
+ ) : (
129
+ preview
130
+ )
131
+ )}
132
+ </View>
133
+ {isUnread && (
134
+ <View style={unreadBadge}>
135
+ <Text style={unreadBadgeText}>{unreadCount}</Text>
136
+ </View>
137
+ )}
138
+ </View>
139
+ </View>
140
+ </Fragment>
141
+ )
142
+ }}
143
+ </Pressable>
144
+ )
145
+ }
@@ -0,0 +1,45 @@
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
+ ChatDateDividerStyle,
6
+ ChatDateDividerTextStyle,
7
+ StyleOverwrite
8
+ } from '../../theme'
9
+
10
+ export type ChatDateDividerProps = Omit<ViewProps, 'children' | 'style'> & {
11
+ children?: ReactNode,
12
+ style?: StyleProp<ViewStyle>,
13
+ dividerStyle?: StyleOverwrite<Record<string, never>, ChatDateDividerStyle>,
14
+ textStyle?: StyleOverwrite<Record<string, never>, ChatDateDividerTextStyle>,
15
+ }
16
+
17
+ export const ChatDateDivider = ({
18
+ children,
19
+ style,
20
+ dividerStyle,
21
+ textStyle,
22
+ ...props
23
+ }: ChatDateDividerProps) => {
24
+ const { theme } = useTheme()
25
+ const state = useMemo(() => ({}), [])
26
+
27
+ const resolvedDividerStyle = useMemo(
28
+ () => theme.components.chat.dateDivider(state, dividerStyle),
29
+ [theme, state, dividerStyle]
30
+ )
31
+ const resolvedTextStyle = useMemo(
32
+ () => theme.components.chat.dateDividerText(state, textStyle),
33
+ [theme, state, textStyle]
34
+ )
35
+
36
+ return (
37
+ <View {...props} style={[resolvedDividerStyle, style]}>
38
+ {typeof children === 'string' || typeof children === 'number' ? (
39
+ <Text style={resolvedTextStyle}>{children}</Text>
40
+ ) : (
41
+ children
42
+ )}
43
+ </View>
44
+ )
45
+ }
@@ -0,0 +1,102 @@
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 { useTheme } from '../../global-contexts/theme'
5
+ import type {
6
+ ChatMessageBubbleContainerStyle,
7
+ ChatMessageBubbleContentStyle,
8
+ ChatMessageBubbleState,
9
+ ChatMessageBubbleStyle,
10
+ ChatMessageBubbleTimestampStyle,
11
+ ChatMessageDirection,
12
+ StyleOverwrite
13
+ } from '../../theme'
14
+
15
+ export type { ChatMessageDirection }
16
+
17
+ export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
18
+ direction?: ChatMessageDirection,
19
+ timestamp?: ReactNode,
20
+ readReceipt?: ReactNode,
21
+ children?: ReactNode,
22
+ style?: StyleProp<ViewStyle>,
23
+ containerStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
24
+ bubbleStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleStyle>,
25
+ contentStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContentStyle>,
26
+ timestampStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleTimestampStyle>,
27
+ }
28
+
29
+ export const ChatMessageBubble = ({
30
+ direction = 'incoming',
31
+ timestamp,
32
+ readReceipt,
33
+ children,
34
+ style,
35
+ containerStyle,
36
+ bubbleStyle,
37
+ contentStyle,
38
+ timestampStyle,
39
+ ...props
40
+ }: ChatMessageBubbleProps) => {
41
+ const { theme } = useTheme()
42
+ const state = useMemo(() => ({ direction }), [direction])
43
+ const staticState = useMemo(() => ({}), [])
44
+
45
+ const resolvedContainerStyle = useMemo(
46
+ () => theme.components.chat.messageBubbleContainer(state, containerStyle),
47
+ [theme, state, containerStyle]
48
+ )
49
+ const resolvedBubbleStyle = useMemo(
50
+ () => theme.components.chat.messageBubble(state, bubbleStyle),
51
+ [theme, state, bubbleStyle]
52
+ )
53
+ const resolvedContentStyle = useMemo(
54
+ () => theme.components.chat.messageBubbleContent(state, contentStyle),
55
+ [theme, state, contentStyle]
56
+ )
57
+ const resolvedTimestampStyle = useMemo(
58
+ () => theme.components.chat.messageBubbleTimestamp(state, timestampStyle),
59
+ [theme, state, timestampStyle]
60
+ )
61
+ const resolvedReceiptStyle = useMemo(
62
+ () => theme.components.chat.messageBubbleReceipt(staticState),
63
+ [theme, staticState]
64
+ )
65
+ const resolvedReceiptTextStyle = useMemo(
66
+ () => theme.components.chat.messageBubbleReceiptText(staticState),
67
+ [theme, staticState]
68
+ )
69
+ const resolvedReceiptIcon = useMemo(
70
+ () => theme.components.chat.messageBubbleReceiptIcon(staticState),
71
+ [theme, staticState]
72
+ )
73
+
74
+ return (
75
+ <View {...props} style={[resolvedContainerStyle, style]}>
76
+ <View style={resolvedBubbleStyle}>
77
+ {typeof children === 'string' || typeof children === 'number' ? (
78
+ <Text style={resolvedContentStyle}>{children}</Text>
79
+ ) : (
80
+ children
81
+ )}
82
+ {timestamp != null && (
83
+ typeof timestamp === 'string' || typeof timestamp === 'number' ? (
84
+ <Text style={resolvedTimestampStyle}>{timestamp}</Text>
85
+ ) : (
86
+ timestamp
87
+ )
88
+ )}
89
+ </View>
90
+ {readReceipt != null && (
91
+ <View style={resolvedReceiptStyle}>
92
+ <CheckCheck size={14} color={resolvedReceiptIcon.color} />
93
+ {typeof readReceipt === 'string' || typeof readReceipt === 'number' ? (
94
+ <Text style={resolvedReceiptTextStyle}>{readReceipt}</Text>
95
+ ) : (
96
+ readReceipt
97
+ )}
98
+ </View>
99
+ )}
100
+ </View>
101
+ )
102
+ }
@@ -0,0 +1,107 @@
1
+ import { useMemo, type ReactNode } from 'react'
2
+ import { Text, View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native'
3
+ import type { ColoringType } from '@helpwave/hightide-design'
4
+ import { useTheme } from '../../global-contexts/theme'
5
+ import type {
6
+ ChatMessageCardState,
7
+ ChatMessageCardStyle,
8
+ ChatMessageCardSubtitleStyle,
9
+ ChatMessageCardTitleStyle,
10
+ ChatMessageDirection,
11
+ StyleOverwrite
12
+ } from '../../theme'
13
+
14
+ export type ChatMessageCardProps = Omit<ViewProps, 'children' | 'style'> & {
15
+ icon?: ReactNode,
16
+ title: ReactNode,
17
+ subtitle?: ReactNode,
18
+ badge?: ReactNode,
19
+ actions?: ReactNode,
20
+ color?: ColoringType,
21
+ direction?: ChatMessageDirection,
22
+ children?: ReactNode,
23
+ style?: StyleProp<ViewStyle>,
24
+ cardStyle?: StyleOverwrite<ChatMessageCardState, ChatMessageCardStyle>,
25
+ titleStyle?: StyleOverwrite<ChatMessageCardState, ChatMessageCardTitleStyle>,
26
+ subtitleStyle?: StyleOverwrite<Record<string, never>, ChatMessageCardSubtitleStyle>,
27
+ }
28
+
29
+ export const ChatMessageCard = ({
30
+ icon,
31
+ title,
32
+ subtitle,
33
+ badge,
34
+ actions,
35
+ color = 'primary',
36
+ direction = 'incoming',
37
+ children,
38
+ style,
39
+ cardStyle,
40
+ titleStyle,
41
+ subtitleStyle,
42
+ ...props
43
+ }: ChatMessageCardProps) => {
44
+ const { theme } = useTheme()
45
+ const state = useMemo(() => ({ direction, color }), [direction, color])
46
+ const staticState = useMemo(() => ({}), [])
47
+
48
+ const resolvedCardStyle = useMemo(
49
+ () => theme.components.chat.messageCard(state, cardStyle),
50
+ [theme, state, cardStyle]
51
+ )
52
+ const resolvedHeaderStyle = useMemo(
53
+ () => theme.components.chat.messageCardHeader(staticState),
54
+ [theme, staticState]
55
+ )
56
+ const resolvedIconStyle = useMemo(
57
+ () => theme.components.chat.messageCardIcon(state),
58
+ [theme, state]
59
+ )
60
+ const resolvedTitleStyle = useMemo(
61
+ () => theme.components.chat.messageCardTitle(state, titleStyle),
62
+ [theme, state, titleStyle]
63
+ )
64
+ const resolvedSubtitleStyle = useMemo(
65
+ () => theme.components.chat.messageCardSubtitle(staticState, subtitleStyle),
66
+ [theme, staticState, subtitleStyle]
67
+ )
68
+ const resolvedBodyStyle = useMemo(
69
+ () => theme.components.chat.messageCardBody(staticState),
70
+ [theme, staticState]
71
+ )
72
+ const resolvedActionsStyle = useMemo(
73
+ () => theme.components.chat.messageCardActions(staticState),
74
+ [theme, staticState]
75
+ )
76
+
77
+ return (
78
+ <View {...props} style={[resolvedCardStyle, style]}>
79
+ <View style={resolvedHeaderStyle}>
80
+ {icon != null && (
81
+ <View style={resolvedIconStyle}>{icon}</View>
82
+ )}
83
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
84
+ {typeof title === 'string' || typeof title === 'number' ? (
85
+ <Text style={resolvedTitleStyle}>{title}</Text>
86
+ ) : (
87
+ title
88
+ )}
89
+ {subtitle != null && (
90
+ typeof subtitle === 'string' || typeof subtitle === 'number' ? (
91
+ <Text style={resolvedSubtitleStyle}>{subtitle}</Text>
92
+ ) : (
93
+ subtitle
94
+ )
95
+ )}
96
+ </View>
97
+ {badge}
98
+ </View>
99
+ {children != null && (
100
+ <View style={resolvedBodyStyle}>{children}</View>
101
+ )}
102
+ {actions != null && (
103
+ <View style={resolvedActionsStyle}>{actions}</View>
104
+ )}
105
+ </View>
106
+ )
107
+ }