@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
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.5",
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-design": "0.0.3",
47
+ "@helpwave/hightide-utils": "0.0.3"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@react-native-async-storage/async-storage": ">=2.0.0",
@@ -0,0 +1,110 @@
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 {
13
+ Download,
14
+ FileText
15
+ } from 'lucide-react-native'
16
+
17
+ import { IconButton } from '@/src/components/user-interaction/IconButton'
18
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
19
+ import type {
20
+ ChatAttachmentCardMetadataStyle,
21
+ ChatAttachmentCardNameStyle,
22
+ ChatAttachmentCardState,
23
+ ChatAttachmentCardStyle,
24
+ ChatMessageDirection
25
+ } from '@/src/theme/types/components/chat'
26
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
27
+
28
+ export type ChatAttachmentCardProps = Omit<ViewProps, 'style'> & {
29
+ name: ReactNode,
30
+ metadata?: ReactNode,
31
+ icon?: ReactNode,
32
+ direction?: ChatMessageDirection,
33
+ downloadLabel?: string,
34
+ onDownload?: () => void,
35
+ style?: StyleProp<ViewStyle>,
36
+ cardStyle?: StyleOverwrite<ChatAttachmentCardState, ChatAttachmentCardStyle>,
37
+ nameStyle?: StyleOverwrite<Record<string, never>, ChatAttachmentCardNameStyle>,
38
+ metadataStyle?: StyleOverwrite<Record<string, never>, ChatAttachmentCardMetadataStyle>,
39
+ }
40
+
41
+ export const ChatAttachmentCard = ({
42
+ name,
43
+ metadata,
44
+ icon,
45
+ direction = 'incoming',
46
+ downloadLabel = 'Download',
47
+ onDownload,
48
+ style,
49
+ cardStyle,
50
+ nameStyle,
51
+ metadataStyle,
52
+ ...props
53
+ }: ChatAttachmentCardProps) => {
54
+ const { theme } = useTheme()
55
+ const state = useMemo(() => ({ direction }), [direction])
56
+ const staticState = useMemo(() => ({}), [])
57
+
58
+ const resolvedCardStyle = useMemo(
59
+ () => theme.components.chat.attachmentCard.container(state, cardStyle),
60
+ [theme, state, cardStyle]
61
+ )
62
+ const resolvedIconStyle = useMemo(
63
+ () => theme.components.chat.attachmentCard.icon(staticState),
64
+ [theme, staticState]
65
+ )
66
+ const resolvedIconColor = useMemo(
67
+ () => theme.components.chat.attachmentCard.iconColor(staticState),
68
+ [theme, staticState]
69
+ )
70
+ const resolvedNameStyle = useMemo(
71
+ () => theme.components.chat.attachmentCard.name(staticState, nameStyle),
72
+ [theme, staticState, nameStyle]
73
+ )
74
+ const resolvedMetadataStyle = useMemo(
75
+ () => theme.components.chat.attachmentCard.metadata(staticState, metadataStyle),
76
+ [theme, staticState, metadataStyle]
77
+ )
78
+
79
+ return (
80
+ <View {...props} style={[resolvedCardStyle, style]}>
81
+ <View style={resolvedIconStyle}>
82
+ {icon ?? <FileText size={22} color={resolvedIconColor.color} />}
83
+ </View>
84
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
85
+ {typeof name === 'string' || typeof name === 'number' ? (
86
+ <Text style={resolvedNameStyle} numberOfLines={1}>{name}</Text>
87
+ ) : (
88
+ name
89
+ )}
90
+ {metadata != null && (
91
+ typeof metadata === 'string' || typeof metadata === 'number' ? (
92
+ <Text style={resolvedMetadataStyle}>{metadata}</Text>
93
+ ) : (
94
+ metadata
95
+ )
96
+ )}
97
+ </View>
98
+ {onDownload && (
99
+ <IconButton
100
+ accessibilityLabel={downloadLabel}
101
+ size="sm"
102
+ color="primary"
103
+ coloringStyle="text"
104
+ onPress={onDownload}
105
+ icon={Download}
106
+ />
107
+ )}
108
+ </View>
109
+ )
110
+ }
@@ -0,0 +1,72 @@
1
+ import {
2
+ useMemo,
3
+ type ReactNode
4
+ } from 'react'
5
+ import {
6
+ ScrollView,
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
+ ChatConversationListFooterStyle,
16
+ ChatConversationListHeaderStyle,
17
+ ChatConversationListStyle
18
+ } from '@/src/theme/types/components/chat'
19
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
20
+
21
+ export type ChatConversationListProps = Omit<ViewProps, 'children' | 'style'> & {
22
+ header?: ReactNode,
23
+ footer?: ReactNode,
24
+ children?: ReactNode,
25
+ style?: StyleProp<ViewStyle>,
26
+ listStyle?: StyleOverwrite<Record<string, never>, ChatConversationListStyle>,
27
+ headerStyle?: StyleOverwrite<Record<string, never>, ChatConversationListHeaderStyle>,
28
+ contentStyle?: StyleProp<ViewStyle>,
29
+ footerStyle?: StyleOverwrite<Record<string, never>, ChatConversationListFooterStyle>,
30
+ }
31
+
32
+ export const ChatConversationList = ({
33
+ header,
34
+ footer,
35
+ children,
36
+ style,
37
+ listStyle,
38
+ headerStyle,
39
+ contentStyle,
40
+ footerStyle,
41
+ ...props
42
+ }: ChatConversationListProps) => {
43
+ const { theme } = useTheme()
44
+ const state = useMemo(() => ({}), [])
45
+
46
+ const resolvedListStyle = useMemo(
47
+ () => theme.components.chat.conversationList.container(state, listStyle),
48
+ [theme, state, listStyle]
49
+ )
50
+ const resolvedHeaderStyle = useMemo(
51
+ () => theme.components.chat.conversationList.header(state, headerStyle),
52
+ [theme, state, headerStyle]
53
+ )
54
+ const resolvedFooterStyle = useMemo(
55
+ () => theme.components.chat.conversationList.footer(state, footerStyle),
56
+ [theme, state, footerStyle]
57
+ )
58
+
59
+ return (
60
+ <View {...props} style={[resolvedListStyle, style]}>
61
+ {header != null && (
62
+ <View style={resolvedHeaderStyle}>{header}</View>
63
+ )}
64
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={contentStyle}>
65
+ {children}
66
+ </ScrollView>
67
+ {footer != null && (
68
+ <View style={resolvedFooterStyle}>{footer}</View>
69
+ )}
70
+ </View>
71
+ )
72
+ }
@@ -0,0 +1,153 @@
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
+ import {
15
+ Check,
16
+ CheckCheck
17
+ } from 'lucide-react-native'
18
+
19
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
20
+ import type {
21
+ ChatConversationRowPreviewStyle,
22
+ ChatConversationRowState,
23
+ ChatConversationRowStyle,
24
+ ChatConversationRowTimestampStyle,
25
+ ChatConversationRowTitleStyle
26
+ } from '@/src/theme/types/components/chat'
27
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
28
+
29
+ export type ChatConversationSentIndicator = 'sent' | 'sentAndReceived'
30
+
31
+ export type ChatConversationRowProps = Omit<PressableProps, 'children' | 'style'> & {
32
+ avatar: ReactNode,
33
+ title: ReactNode,
34
+ timestamp?: ReactNode,
35
+ preview?: ReactNode,
36
+ unreadCount?: number,
37
+ isSelected?: boolean,
38
+ sentIndicator?: ChatConversationSentIndicator,
39
+ style?: StyleProp<ViewStyle>,
40
+ rowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowStyle>,
41
+ titleStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTitleStyle>,
42
+ timestampStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTimestampStyle>,
43
+ previewStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowPreviewStyle>,
44
+ }
45
+
46
+ type PressableInteraction = {
47
+ pressed: boolean,
48
+ hovered?: boolean,
49
+ focused?: boolean,
50
+ }
51
+
52
+ export const ChatConversationRow = ({
53
+ avatar,
54
+ title,
55
+ timestamp,
56
+ preview,
57
+ unreadCount,
58
+ isSelected = false,
59
+ sentIndicator,
60
+ disabled,
61
+ style,
62
+ rowStyle,
63
+ titleStyle,
64
+ timestampStyle,
65
+ previewStyle,
66
+ ...props
67
+ }: ChatConversationRowProps) => {
68
+ const { theme } = useTheme()
69
+ const isUnread = (unreadCount ?? 0) > 0
70
+ const SentIndicatorIcon = sentIndicator === 'sentAndReceived' ? CheckCheck : Check
71
+
72
+ const resolveState = (interaction: PressableInteraction): ChatConversationRowState => ({
73
+ isUnread,
74
+ isSelected,
75
+ isDisabled: !!disabled,
76
+ isPressed: interaction.pressed,
77
+ isHovered: !!interaction.hovered,
78
+ isFocused: !!interaction.focused,
79
+ })
80
+
81
+ const staticState = useMemo(() => ({}), [])
82
+ const unreadBadge = useMemo(
83
+ () => theme.components.chat.conversationRow.unreadBadge(staticState),
84
+ [theme, staticState]
85
+ )
86
+ const unreadBadgeText = useMemo(
87
+ () => theme.components.chat.conversationRow.unreadBadgeText(staticState),
88
+ [theme, staticState]
89
+ )
90
+ const sentIndicatorColor = useMemo(
91
+ () => theme.components.chat.conversationRow.sentIndicator(staticState).color,
92
+ [theme, staticState]
93
+ )
94
+
95
+ return (
96
+ <Pressable
97
+ {...props}
98
+ disabled={disabled}
99
+ style={(pressableState) => {
100
+ const state = resolveState(pressableState as PressableInteraction)
101
+ return [theme.components.chat.conversationRow.container(state, rowStyle), style]
102
+ }}
103
+ >
104
+ {(pressableState) => {
105
+ const state = resolveState(pressableState as PressableInteraction)
106
+ const resolvedTitle = theme.components.chat.conversationRow.title(state, titleStyle)
107
+ const resolvedTimestamp = theme.components.chat.conversationRow.timestamp(state, timestampStyle)
108
+ const resolvedPreview = theme.components.chat.conversationRow.preview(state, previewStyle)
109
+
110
+ return (
111
+ <Fragment>
112
+ {avatar}
113
+ <View style={{ flex: 1, minWidth: 0, gap: 5 }}>
114
+ <View style={{ flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', gap: 8 }}>
115
+ {typeof title === 'string' || typeof title === 'number' ? (
116
+ <Text style={resolvedTitle} numberOfLines={1}>{title}</Text>
117
+ ) : (
118
+ title
119
+ )}
120
+ {timestamp != null && (
121
+ typeof timestamp === 'string' || typeof timestamp === 'number' ? (
122
+ <Text style={resolvedTimestamp}>{timestamp}</Text>
123
+ ) : (
124
+ timestamp
125
+ )
126
+ )}
127
+ </View>
128
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
129
+ <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', gap: 4, minWidth: 0 }}>
130
+ {sentIndicator && (
131
+ <SentIndicatorIcon size={14} color={sentIndicatorColor} />
132
+ )}
133
+ {preview != null && (
134
+ typeof preview === 'string' || typeof preview === 'number' ? (
135
+ <Text style={resolvedPreview} numberOfLines={1}>{preview}</Text>
136
+ ) : (
137
+ preview
138
+ )
139
+ )}
140
+ </View>
141
+ {isUnread && (
142
+ <View style={unreadBadge}>
143
+ <Text style={unreadBadgeText}>{unreadCount}</Text>
144
+ </View>
145
+ )}
146
+ </View>
147
+ </View>
148
+ </Fragment>
149
+ )
150
+ }}
151
+ </Pressable>
152
+ )
153
+ }
@@ -0,0 +1,55 @@
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
+ ChatDateDividerStyle,
16
+ ChatDateDividerTextStyle
17
+ } from '@/src/theme/types/components/chat'
18
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
19
+
20
+ export type ChatDateDividerProps = Omit<ViewProps, 'children' | 'style'> & {
21
+ children?: ReactNode,
22
+ style?: StyleProp<ViewStyle>,
23
+ dividerStyle?: StyleOverwrite<Record<string, never>, ChatDateDividerStyle>,
24
+ textStyle?: StyleOverwrite<Record<string, never>, ChatDateDividerTextStyle>,
25
+ }
26
+
27
+ export const ChatDateDivider = ({
28
+ children,
29
+ style,
30
+ dividerStyle,
31
+ textStyle,
32
+ ...props
33
+ }: ChatDateDividerProps) => {
34
+ const { theme } = useTheme()
35
+ const state = useMemo(() => ({}), [])
36
+
37
+ const resolvedDividerStyle = useMemo(
38
+ () => theme.components.chat.dateDivider.container(state, dividerStyle),
39
+ [theme, state, dividerStyle]
40
+ )
41
+ const resolvedTextStyle = useMemo(
42
+ () => theme.components.chat.dateDivider.text(state, textStyle),
43
+ [theme, state, textStyle]
44
+ )
45
+
46
+ return (
47
+ <View {...props} style={[resolvedDividerStyle, style]}>
48
+ {typeof children === 'string' || typeof children === 'number' ? (
49
+ <Text style={resolvedTextStyle}>{children}</Text>
50
+ ) : (
51
+ children
52
+ )}
53
+ </View>
54
+ )
55
+ }
@@ -0,0 +1,112 @@
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 { useTheme } from '@/src/global-contexts/theme/ThemeContext'
15
+ import type {
16
+ ChatMessageBubbleContainerStyle,
17
+ ChatMessageBubbleContentStyle,
18
+ ChatMessageBubbleState,
19
+ ChatMessageBubbleStyle,
20
+ ChatMessageBubbleTimestampStyle,
21
+ ChatMessageDirection
22
+ } from '@/src/theme/types/components/chat'
23
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
24
+
25
+ export type { ChatMessageDirection }
26
+
27
+ export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
28
+ direction?: ChatMessageDirection,
29
+ timestamp?: ReactNode,
30
+ readReceipt?: ReactNode,
31
+ children?: ReactNode,
32
+ style?: StyleProp<ViewStyle>,
33
+ containerStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
34
+ bubbleStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleStyle>,
35
+ contentStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContentStyle>,
36
+ timestampStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleTimestampStyle>,
37
+ }
38
+
39
+ export const ChatMessageBubble = ({
40
+ direction = 'incoming',
41
+ timestamp,
42
+ readReceipt,
43
+ children,
44
+ style,
45
+ containerStyle,
46
+ bubbleStyle,
47
+ contentStyle,
48
+ timestampStyle,
49
+ ...props
50
+ }: ChatMessageBubbleProps) => {
51
+ const { theme } = useTheme()
52
+ const state = useMemo(() => ({ direction }), [direction])
53
+ const staticState = useMemo(() => ({}), [])
54
+
55
+ const resolvedContainerStyle = useMemo(
56
+ () => theme.components.chat.messageBubble.container(state, containerStyle),
57
+ [theme, state, containerStyle]
58
+ )
59
+ const resolvedBubbleStyle = useMemo(
60
+ () => theme.components.chat.messageBubble.bubble(state, bubbleStyle),
61
+ [theme, state, bubbleStyle]
62
+ )
63
+ const resolvedContentStyle = useMemo(
64
+ () => theme.components.chat.messageBubble.content(state, contentStyle),
65
+ [theme, state, contentStyle]
66
+ )
67
+ const resolvedTimestampStyle = useMemo(
68
+ () => theme.components.chat.messageBubble.timestamp(state, timestampStyle),
69
+ [theme, state, timestampStyle]
70
+ )
71
+ const resolvedReceiptStyle = useMemo(
72
+ () => theme.components.chat.messageBubble.receipt(staticState),
73
+ [theme, staticState]
74
+ )
75
+ const resolvedReceiptTextStyle = useMemo(
76
+ () => theme.components.chat.messageBubble.receiptText(staticState),
77
+ [theme, staticState]
78
+ )
79
+ const resolvedReceiptIcon = useMemo(
80
+ () => theme.components.chat.messageBubble.receiptIcon(staticState),
81
+ [theme, staticState]
82
+ )
83
+
84
+ return (
85
+ <View {...props} style={[resolvedContainerStyle, style]}>
86
+ <View style={resolvedBubbleStyle}>
87
+ {typeof children === 'string' || typeof children === 'number' ? (
88
+ <Text style={resolvedContentStyle}>{children}</Text>
89
+ ) : (
90
+ children
91
+ )}
92
+ {timestamp != null && (
93
+ typeof timestamp === 'string' || typeof timestamp === 'number' ? (
94
+ <Text style={resolvedTimestampStyle}>{timestamp}</Text>
95
+ ) : (
96
+ timestamp
97
+ )
98
+ )}
99
+ </View>
100
+ {readReceipt != null && (
101
+ <View style={resolvedReceiptStyle}>
102
+ <CheckCheck size={14} color={resolvedReceiptIcon.color} />
103
+ {typeof readReceipt === 'string' || typeof readReceipt === 'number' ? (
104
+ <Text style={resolvedReceiptTextStyle}>{readReceipt}</Text>
105
+ ) : (
106
+ readReceipt
107
+ )}
108
+ </View>
109
+ )}
110
+ </View>
111
+ )
112
+ }
@@ -0,0 +1,118 @@
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 type { ColoringType } from '@helpwave/hightide-design/helpers'
14
+
15
+ import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
16
+ import type {
17
+ ChatMessageCardState,
18
+ ChatMessageCardStyle,
19
+ ChatMessageCardSubtitleStyle,
20
+ ChatMessageCardTitleStyle,
21
+ ChatMessageDirection
22
+ } from '@/src/theme/types/components/chat'
23
+ import type { StyleOverwrite } from '@/src/theme/types/resolver'
24
+
25
+ export type ChatMessageCardProps = Omit<ViewProps, 'children' | 'style'> & {
26
+ icon?: ReactNode,
27
+ title: ReactNode,
28
+ subtitle?: ReactNode,
29
+ badge?: ReactNode,
30
+ actions?: ReactNode,
31
+ color?: ColoringType,
32
+ direction?: ChatMessageDirection,
33
+ children?: ReactNode,
34
+ style?: StyleProp<ViewStyle>,
35
+ cardStyle?: StyleOverwrite<ChatMessageCardState, ChatMessageCardStyle>,
36
+ titleStyle?: StyleOverwrite<ChatMessageCardState, ChatMessageCardTitleStyle>,
37
+ subtitleStyle?: StyleOverwrite<Record<string, never>, ChatMessageCardSubtitleStyle>,
38
+ }
39
+
40
+ export const ChatMessageCard = ({
41
+ icon,
42
+ title,
43
+ subtitle,
44
+ badge,
45
+ actions,
46
+ color = 'primary',
47
+ direction = 'incoming',
48
+ children,
49
+ style,
50
+ cardStyle,
51
+ titleStyle,
52
+ subtitleStyle,
53
+ ...props
54
+ }: ChatMessageCardProps) => {
55
+ const { theme } = useTheme()
56
+ const state = useMemo(() => ({ direction, color }), [direction, color])
57
+ const staticState = useMemo(() => ({}), [])
58
+
59
+ const resolvedCardStyle = useMemo(
60
+ () => theme.components.chat.messageCard.container(state, cardStyle),
61
+ [theme, state, cardStyle]
62
+ )
63
+ const resolvedHeaderStyle = useMemo(
64
+ () => theme.components.chat.messageCard.header(staticState),
65
+ [theme, staticState]
66
+ )
67
+ const resolvedIconStyle = useMemo(
68
+ () => theme.components.chat.messageCard.icon(state),
69
+ [theme, state]
70
+ )
71
+ const resolvedTitleStyle = useMemo(
72
+ () => theme.components.chat.messageCard.title(state, titleStyle),
73
+ [theme, state, titleStyle]
74
+ )
75
+ const resolvedSubtitleStyle = useMemo(
76
+ () => theme.components.chat.messageCard.subtitle(staticState, subtitleStyle),
77
+ [theme, staticState, subtitleStyle]
78
+ )
79
+ const resolvedBodyStyle = useMemo(
80
+ () => theme.components.chat.messageCard.body(staticState),
81
+ [theme, staticState]
82
+ )
83
+ const resolvedActionsStyle = useMemo(
84
+ () => theme.components.chat.messageCard.actions(staticState),
85
+ [theme, staticState]
86
+ )
87
+
88
+ return (
89
+ <View {...props} style={[resolvedCardStyle, style]}>
90
+ <View style={resolvedHeaderStyle}>
91
+ {icon != null && (
92
+ <View style={resolvedIconStyle}>{icon}</View>
93
+ )}
94
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
95
+ {typeof title === 'string' || typeof title === 'number' ? (
96
+ <Text style={resolvedTitleStyle}>{title}</Text>
97
+ ) : (
98
+ title
99
+ )}
100
+ {subtitle != null && (
101
+ typeof subtitle === 'string' || typeof subtitle === 'number' ? (
102
+ <Text style={resolvedSubtitleStyle}>{subtitle}</Text>
103
+ ) : (
104
+ subtitle
105
+ )
106
+ )}
107
+ </View>
108
+ {badge}
109
+ </View>
110
+ {children != null && (
111
+ <View style={resolvedBodyStyle}>{children}</View>
112
+ )}
113
+ {actions != null && (
114
+ <View style={resolvedActionsStyle}>{actions}</View>
115
+ )}
116
+ </View>
117
+ )
118
+ }