@helpwave/hightide-native 0.1.1 → 0.3.0

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 (65) hide show
  1. package/package.json +2 -2
  2. package/src/components/chat/ChatAttachmentMessageBubble.tsx +3 -0
  3. package/src/components/chat/ChatConversationRow.tsx +127 -27
  4. package/src/components/chat/ChatQuickReplyChip.tsx +7 -1
  5. package/src/components/chat/ChatThreadHeader.tsx +88 -48
  6. package/src/components/list/ListActionItem.tsx +60 -11
  7. package/src/components/list/ListItem.tsx +47 -21
  8. package/src/components/list/ListItemAccessory.tsx +34 -0
  9. package/src/components/list/ListItemTextContent.tsx +53 -0
  10. package/src/components/list/ListNavigationItem.tsx +60 -16
  11. package/src/components/list/index.ts +2 -0
  12. package/src/components/user-interaction/Button.tsx +6 -15
  13. package/src/components/user-interaction/IconButton.tsx +5 -7
  14. package/src/components/user-interaction/MultiSelect.tsx +72 -46
  15. package/src/components/user-interaction/Select.tsx +5 -1
  16. package/src/components/user-interaction/ThemedPressable.tsx +9 -4
  17. package/src/components/visualization-and-display/Chip.tsx +12 -5
  18. package/src/components/visualization-and-display/ThemedIcon.tsx +12 -8
  19. package/src/components/visualization-and-display/ThemedText.tsx +6 -2
  20. package/src/global-contexts/HightideProvider.tsx +9 -17
  21. package/src/global-contexts/content-theme/ContentThemeContext.ts +5 -2
  22. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +64 -10
  23. package/src/global-contexts/theme/ThemeProvider.tsx +9 -4
  24. package/src/theme/adapters/index.ts +1 -4
  25. package/src/theme/adapters/style-adapter-utils.ts +433 -0
  26. package/src/theme/resolvers/avatar.ts +9 -10
  27. package/src/theme/resolvers/button.ts +6 -7
  28. package/src/theme/resolvers/card.ts +3 -2
  29. package/src/theme/resolvers/chat/attachment-message-bubble.ts +25 -37
  30. package/src/theme/resolvers/chat/conversation-list.ts +5 -4
  31. package/src/theme/resolvers/chat/conversation-row.ts +114 -14
  32. package/src/theme/resolvers/chat/date-divider.ts +4 -4
  33. package/src/theme/resolvers/chat/message-bubble.ts +9 -10
  34. package/src/theme/resolvers/chat/message-composer.ts +5 -6
  35. package/src/theme/resolvers/chat/message-list.ts +3 -2
  36. package/src/theme/resolvers/chat/quick-reply-chip.ts +12 -20
  37. package/src/theme/resolvers/chat/system-line.ts +5 -6
  38. package/src/theme/resolvers/chat/thread-header.ts +60 -12
  39. package/src/theme/resolvers/checkbox.ts +6 -6
  40. package/src/theme/resolvers/chip.ts +9 -4
  41. package/src/theme/resolvers/divider.ts +3 -3
  42. package/src/theme/resolvers/iconButton.ts +5 -10
  43. package/src/theme/resolvers/input.ts +12 -7
  44. package/src/theme/resolvers/listItem.ts +19 -16
  45. package/src/theme/resolvers/multiSelect.ts +23 -16
  46. package/src/theme/resolvers/searchBar.ts +14 -7
  47. package/src/theme/resolvers/select.ts +22 -15
  48. package/src/theme/resolvers/switch.ts +5 -4
  49. package/src/theme/resolvers/themedPressable.ts +10 -5
  50. package/src/theme/themes/createHightideTheme.ts +13 -4
  51. package/src/theme/types/components/chat.ts +63 -3
  52. package/src/theme/types/components/chip.ts +4 -0
  53. package/src/theme/types/components/iconButton.ts +1 -7
  54. package/src/theme/types/components/listItem.ts +5 -0
  55. package/src/theme/types/components/multiSelect.ts +1 -0
  56. package/src/theme/types/components/select.ts +1 -0
  57. package/src/theme/types/components/themedPressable.ts +4 -0
  58. package/src/theme/types/layout.ts +13 -7
  59. package/src/theme/types/semantics.ts +2 -1
  60. package/src/theme/types/theme.ts +40 -12
  61. package/src/theme/types/typography.ts +9 -4
  62. package/src/theme/adapters/container-adapter.ts +0 -323
  63. package/src/theme/adapters/defined.ts +0 -11
  64. package/src/theme/adapters/icon-style-adapter.ts +0 -10
  65. package/src/theme/adapters/text-style-adapter.ts +0 -16
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "access": "public"
11
11
  },
12
12
  "license": "AGPL-3.0-only",
13
- "version": "0.1.1",
13
+ "version": "0.3.0",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "src"
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "lucide-react-native": "0.561.0",
46
- "@helpwave/hightide-design": "0.1.1",
46
+ "@helpwave/hightide-design": "0.3.0",
47
47
  "@helpwave/hightide-utils": "0.1.0"
48
48
  },
49
49
  "peerDependencies": {
@@ -205,6 +205,9 @@ export const ChatAttachmentMessageBubble = ({
205
205
  textStyle={(_, pressableState) => (
206
206
  contentResolvers.text(pressableState)
207
207
  )}
208
+ iconStyle={(_, pressableState) => (
209
+ contentResolvers.icon(pressableState)
210
+ )}
208
211
  >
209
212
  <View style={resolvedFileIconContainerStyle}>
210
213
  {icon ?? (
@@ -1,6 +1,9 @@
1
1
  import {
2
+ cloneElement,
2
3
  Fragment,
4
+ isValidElement,
3
5
  useMemo,
6
+ type ReactElement,
4
7
  type ReactNode
5
8
  } from 'react'
6
9
  import {
@@ -14,14 +17,24 @@ import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
14
17
  import { ThemedText } from '../visualization-and-display/ThemedText'
15
18
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
16
19
  import type {
20
+ AvatarState,
21
+ AvatarStyle
22
+ } from '../../theme/types/components/avatar'
23
+ import type {
24
+ ChatConversationRowContentContainerStyle,
25
+ ChatConversationRowHeaderRowStyle,
26
+ ChatConversationRowMessageRowStyle,
17
27
  ChatConversationRowPreviewStyle,
28
+ ChatConversationRowSentIndicatorStyle,
18
29
  ChatConversationRowState,
19
- ChatConversationRowStyle,
20
30
  ChatConversationRowTimestampStyle,
21
- ChatConversationRowTitleStyle
31
+ ChatConversationRowTitleStyle,
32
+ PressableContainerStyle,
33
+ PressableState
22
34
  } from '../../theme/types/components/chat'
23
35
  import type { StyleOverwrite } from '../../theme/types/resolver'
24
36
  import { ThemedPressable } from '../user-interaction'
37
+ import type { AvatarProps } from '../visualization-and-display/Avatar'
25
38
 
26
39
  export type ChatConversationSentIndicator = 'sent' | 'sentAndReceived'
27
40
 
@@ -34,10 +47,15 @@ export type ChatConversationRowProps = Omit<PressableProps, 'children' | 'style'
34
47
  isSelected?: boolean,
35
48
  sentIndicator?: ChatConversationSentIndicator,
36
49
  style?: StyleProp<ViewStyle>,
37
- rowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowStyle>,
50
+ rowStyle?: StyleOverwrite<PressableState, PressableContainerStyle>,
51
+ contentContainerStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowContentContainerStyle>,
52
+ headerRowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowHeaderRowStyle>,
53
+ messageRowStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowMessageRowStyle>,
38
54
  titleStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTitleStyle>,
39
55
  timestampStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowTimestampStyle>,
40
56
  previewStyle?: StyleOverwrite<ChatConversationRowState, ChatConversationRowPreviewStyle>,
57
+ sentIndicatorStyle?: StyleOverwrite<Record<string, never>, ChatConversationRowSentIndicatorStyle>,
58
+ avatarStyle?: StyleOverwrite<AvatarState, AvatarStyle>,
41
59
  }
42
60
 
43
61
  type PressableInteraction = {
@@ -47,6 +65,14 @@ type PressableInteraction = {
47
65
  focusVisible?: boolean,
48
66
  }
49
67
 
68
+ type AvatarElementProps = AvatarProps & {
69
+ avatarStyle?: AvatarProps['avatarStyle'],
70
+ }
71
+
72
+ const toNumericSize = (value: unknown): number | undefined => (
73
+ typeof value === 'number' ? value : undefined
74
+ )
75
+
50
76
  export const ChatConversationRow = ({
51
77
  avatar,
52
78
  title,
@@ -58,9 +84,14 @@ export const ChatConversationRow = ({
58
84
  disabled,
59
85
  style,
60
86
  rowStyle,
87
+ contentContainerStyle,
88
+ headerRowStyle,
89
+ messageRowStyle,
61
90
  titleStyle,
62
91
  timestampStyle,
63
92
  previewStyle,
93
+ sentIndicatorStyle,
94
+ avatarStyle,
64
95
  ...props
65
96
  }: ChatConversationRowProps) => {
66
97
  const { theme } = useTheme()
@@ -88,31 +119,101 @@ export const ChatConversationRow = ({
88
119
  () => theme.components.chat.conversationRow.unreadBadgeText(staticState),
89
120
  [theme, staticState]
90
121
  )
91
- const sentIndicatorColor = useMemo(
92
- () => theme.components.chat.conversationRow.sentIndicator(staticState).color,
122
+ const resolvedSentIndicator = useMemo(
123
+ () => theme.components.chat.conversationRow.sentIndicator(staticState, sentIndicatorStyle),
124
+ [theme, staticState, sentIndicatorStyle]
125
+ )
126
+ const avatarTheme = useMemo(
127
+ () => theme.components.chat.conversationRow.avatar(staticState),
93
128
  [theme, staticState]
94
129
  )
130
+ const avatarSize = useMemo(
131
+ () => toNumericSize(avatarTheme.container({}).width),
132
+ [avatarTheme]
133
+ )
134
+
135
+ const resolvedAvatar = useMemo(() => {
136
+ if (!isValidElement(avatar) || avatarSize === undefined) {
137
+ return avatar
138
+ }
139
+
140
+ const avatarElement = avatar as ReactElement<AvatarElementProps>
141
+
142
+ return cloneElement(avatarElement, {
143
+ size: avatarSize,
144
+ avatarStyle: (_, avatarState) => avatarTheme.container(
145
+ {
146
+ ...avatarState,
147
+ size: avatarSize,
148
+ },
149
+ avatarStyle ?? avatarElement.props.avatarStyle
150
+ ),
151
+ imageStyle: (_, avatarState) => avatarTheme.image(
152
+ {
153
+ ...avatarState,
154
+ size: avatarSize,
155
+ },
156
+ avatarElement.props.imageStyle
157
+ ),
158
+ textStyle: (_, avatarState) => avatarTheme.text(
159
+ {
160
+ ...avatarState,
161
+ size: avatarSize,
162
+ },
163
+ avatarElement.props.textStyle
164
+ ),
165
+ iconStyle: (_, avatarState) => avatarTheme.icon(
166
+ {
167
+ ...avatarState,
168
+ size: avatarSize,
169
+ },
170
+ avatarElement.props.iconStyle
171
+ ),
172
+ })
173
+ }, [avatar, avatarSize, avatarStyle, avatarTheme])
95
174
 
96
175
  return (
97
176
  <ThemedPressable
98
177
  {...props}
99
178
  disabled={disabled}
100
- style={(pressableState) => {
179
+ style={style}
180
+ containerStyle={(_, pressableState) => {
101
181
  const state = resolveState(pressableState as PressableInteraction)
102
- return [theme.components.chat.conversationRow.container(state, rowStyle), style]
182
+ return theme.components.chat.conversationRow.pressable(state).container(
183
+ pressableState,
184
+ rowStyle
185
+ )
186
+ }}
187
+ stateLayerStyle={(_, pressableState) => {
188
+ const state = resolveState(pressableState as PressableInteraction)
189
+ return theme.components.chat.conversationRow.pressable(state).stateLayer(pressableState)
190
+ }}
191
+ textStyle={(_, pressableState) => {
192
+ const state = resolveState(pressableState as PressableInteraction)
193
+ return theme.components.chat.conversationRow.pressable(state).text(pressableState)
194
+ }}
195
+ iconStyle={(_, pressableState) => {
196
+ const state = resolveState(pressableState as PressableInteraction)
197
+ return theme.components.chat.conversationRow.pressable(state).icon(pressableState)
103
198
  }}
104
199
  >
105
200
  {(pressableState) => {
106
201
  const state = resolveState(pressableState as PressableInteraction)
202
+ const resolvedContentContainer = theme.components.chat.conversationRow.contentContainer(
203
+ state,
204
+ contentContainerStyle
205
+ )
206
+ const resolvedHeaderRow = theme.components.chat.conversationRow.headerRow(state, headerRowStyle)
207
+ const resolvedMessageRow = theme.components.chat.conversationRow.messageRow(state, messageRowStyle)
107
208
  const resolvedTitle = theme.components.chat.conversationRow.title(state, titleStyle)
108
209
  const resolvedTimestamp = theme.components.chat.conversationRow.timestamp(state, timestampStyle)
109
210
  const resolvedPreview = theme.components.chat.conversationRow.preview(state, previewStyle)
110
211
 
111
212
  return (
112
213
  <Fragment>
113
- {avatar}
114
- <View style={{ flex: 1, minWidth: 0, gap: 5 }}>
115
- <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
214
+ {resolvedAvatar}
215
+ <View style={resolvedContentContainer}>
216
+ <View style={resolvedHeaderRow}>
116
217
  {typeof title === 'string' || typeof title === 'number' ? (
117
218
  <ThemedText style={resolvedTitle} numberOfLines={1}>{title}</ThemedText>
118
219
  ) : (
@@ -126,23 +227,22 @@ export const ChatConversationRow = ({
126
227
  )
127
228
  )}
128
229
  </View>
129
- <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
130
- <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', gap: 4, minWidth: 0 }}>
131
- {sentIndicator && (
132
- <ThemedIcon
133
- icon={sentIndicatorIcon}
134
- size={16}
135
- color={sentIndicatorColor}
136
- />
137
- )}
138
- {preview != null && (
139
- typeof preview === 'string' || typeof preview === 'number' ? (
140
- <ThemedText style={resolvedPreview} numberOfLines={1}>{preview}</ThemedText>
141
- ) : (
142
- preview
143
- )
144
- )}
145
- </View>
230
+ <View style={resolvedMessageRow}>
231
+ {sentIndicator && (
232
+ <ThemedIcon
233
+ icon={sentIndicatorIcon}
234
+ size={resolvedSentIndicator.size}
235
+ strokeWidth={resolvedSentIndicator.strokeWidth}
236
+ color={resolvedSentIndicator.color}
237
+ />
238
+ )}
239
+ {preview != null && (
240
+ typeof preview === 'string' || typeof preview === 'number' ? (
241
+ <ThemedText style={resolvedPreview} numberOfLines={1}>{preview}</ThemedText>
242
+ ) : (
243
+ preview
244
+ )
245
+ )}
146
246
  {isUnread && (
147
247
  <View style={unreadBadge}>
148
248
  <ThemedText style={unreadBadgeText}>{unreadCount}</ThemedText>
@@ -13,6 +13,7 @@ import { ThemedText } from '../visualization-and-display/ThemedText'
13
13
  import type {
14
14
  ChatQuickReplyChipState,
15
15
  PressableContainerStyle,
16
+ PressableIconStyle,
16
17
  PressableState,
17
18
  PressableTextStyle
18
19
  } from '../../theme/types/components/chat'
@@ -20,13 +21,14 @@ import type { StyleOverwrite } from '../../theme/types/resolver'
20
21
 
21
22
  export type ChatQuickReplyChipProps = Omit<
22
23
  ThemedPressableProps,
23
- 'children' | 'containerStyle' | 'textStyle' | 'stateLayerStyle'
24
+ 'children' | 'containerStyle' | 'textStyle' | 'stateLayerStyle' | 'iconStyle'
24
25
  > & {
25
26
  isActive?: boolean,
26
27
  children?: ReactNode,
27
28
  style?: StyleProp<ViewStyle>,
28
29
  containerStyle?: StyleOverwrite<PressableState, PressableContainerStyle>,
29
30
  textStyle?: StyleOverwrite<PressableState, PressableTextStyle>,
31
+ iconStyle?: StyleOverwrite<PressableState, PressableIconStyle>,
30
32
  }
31
33
 
32
34
  export const ChatQuickReplyChip = ({
@@ -36,6 +38,7 @@ export const ChatQuickReplyChip = ({
36
38
  style,
37
39
  containerStyle,
38
40
  textStyle,
41
+ iconStyle,
39
42
  ...props
40
43
  }: ChatQuickReplyChipProps) => {
41
44
  const { theme } = useTheme()
@@ -59,6 +62,9 @@ export const ChatQuickReplyChip = ({
59
62
  textStyle={(_, pressableState) => (
60
63
  pressableResolvers.text(pressableState, textStyle)
61
64
  )}
65
+ iconStyle={(_, pressableState) => (
66
+ pressableResolvers.icon(pressableState, iconStyle)
67
+ )}
62
68
  >
63
69
  {typeof children === 'string' || typeof children === 'number' ? (
64
70
  <ThemedText>{children}</ThemedText>
@@ -15,11 +15,17 @@ import {
15
15
  Avatar,
16
16
  type AvatarProps
17
17
  } from '../visualization-and-display/Avatar'
18
+ import {
19
+ ThemedPressable,
20
+ type ThemedPressableProps
21
+ } from '../user-interaction/ThemedPressable'
18
22
  import type {
19
23
  ChatThreadHeaderContentRowStyle,
20
24
  ChatThreadHeaderStyle,
21
25
  ChatThreadHeaderSubtitleStyle,
22
- ChatThreadHeaderTitleStyle
26
+ ChatThreadHeaderTitleStyle,
27
+ PressableContainerStyle,
28
+ PressableState
23
29
  } from '../../theme/types/components/chat'
24
30
  import type { StyleOverwrite } from '../../theme/types/resolver'
25
31
 
@@ -29,11 +35,14 @@ export type ChatThreadHeaderProps = Omit<ViewProps, 'style'> & {
29
35
  subtitle?: ReactNode,
30
36
  leftActions?: ReactNode,
31
37
  rightActions?: ReactNode,
38
+ onPress?: ThemedPressableProps['onPress'],
39
+ disabled?: boolean,
32
40
  style?: StyleProp<ViewStyle>,
33
41
  headerStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderStyle>,
34
42
  contentRowStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderContentRowStyle>,
35
43
  titleStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderTitleStyle>,
36
44
  subtitleStyle?: StyleOverwrite<Record<string, never>, ChatThreadHeaderSubtitleStyle>,
45
+ pressableContainerStyle?: StyleOverwrite<PressableState, PressableContainerStyle>,
37
46
  }
38
47
 
39
48
  export const ChatThreadHeader = ({
@@ -42,11 +51,14 @@ export const ChatThreadHeader = ({
42
51
  subtitle,
43
52
  leftActions,
44
53
  rightActions,
54
+ onPress,
55
+ disabled,
45
56
  style,
46
57
  headerStyle,
47
58
  contentRowStyle,
48
59
  titleStyle,
49
60
  subtitleStyle,
61
+ pressableContainerStyle,
50
62
  ...props
51
63
  }: ChatThreadHeaderProps) => {
52
64
  const { theme } = useTheme()
@@ -71,16 +83,26 @@ export const ChatThreadHeader = ({
71
83
  () => theme.components.chat.threadHeader.subtitle(state, subtitleStyle),
72
84
  [theme, state, subtitleStyle]
73
85
  )
74
-
75
- const avatarThemeState = useMemo(() => ({
76
- size: resolvedAvatar.size,
77
- color: resolvedAvatar.color,
78
- }), [resolvedAvatar.size, resolvedAvatar.color])
86
+ const pressableResolvers = useMemo(
87
+ () => theme.components.chat.threadHeader.pressable(state),
88
+ [theme, state]
89
+ )
79
90
 
80
91
  const avatarTheme = useMemo(
81
- () => theme.components.chat.threadHeader.avatar(avatarThemeState),
82
- [theme, avatarThemeState]
92
+ () => theme.components.chat.threadHeader.avatar({
93
+ size: resolvedAvatar.size,
94
+ color: resolvedAvatar.color,
95
+ }),
96
+ [theme, resolvedAvatar.size, resolvedAvatar.color]
83
97
  )
98
+ const avatarSize = useMemo(() => {
99
+ if (typeof resolvedAvatar.size === 'number' || typeof resolvedAvatar.size === 'string') {
100
+ return resolvedAvatar.size
101
+ }
102
+
103
+ const width = avatarTheme.container({}).width
104
+ return typeof width === 'number' ? width : resolvedAvatar.size
105
+ }, [avatarTheme, resolvedAvatar.size])
84
106
 
85
107
  return (
86
108
  <View {...props} style={[resolvedHeaderStyle, style]}>
@@ -89,51 +111,69 @@ export const ChatThreadHeader = ({
89
111
  {leftActions}
90
112
  </View>
91
113
  )}
92
- <Avatar
93
- {...resolvedAvatar}
94
- avatarStyle={(_, avatarState) => avatarTheme.container(
95
- {
96
- ...avatarState,
97
- size: resolvedAvatar.size,
98
- },
99
- resolvedAvatar.avatarStyle
114
+ <ThemedPressable
115
+ onPress={onPress}
116
+ disabled={disabled}
117
+ containerStyle={(_, pressableState) => (
118
+ pressableResolvers.container(pressableState, pressableContainerStyle)
100
119
  )}
101
- imageStyle={(_, avatarState) => avatarTheme.image(
102
- {
103
- ...avatarState,
104
- size: resolvedAvatar.size,
105
- },
106
- resolvedAvatar.imageStyle
120
+ stateLayerStyle={(_, pressableState) => (
121
+ pressableResolvers.stateLayer(pressableState)
107
122
  )}
108
- textStyle={(_, avatarState) => avatarTheme.text(
109
- {
110
- ...avatarState,
111
- size: resolvedAvatar.size,
112
- },
113
- resolvedAvatar.textStyle
123
+ textStyle={(_, pressableState) => (
124
+ pressableResolvers.text(pressableState)
114
125
  )}
115
- iconStyle={(_, avatarState) => avatarTheme.icon(
116
- {
117
- ...avatarState,
118
- size: resolvedAvatar.size,
119
- },
120
- resolvedAvatar.iconStyle
126
+ iconStyle={(_, pressableState) => (
127
+ pressableResolvers.icon(pressableState)
121
128
  )}
122
- />
123
- <View style={resolvedContentRowStyle}>
124
- {typeof title === 'string' || typeof title === 'number' ? (
125
- <ThemedText style={resolvedTitleStyle} numberOfLines={1}>{title}</ThemedText>
126
- ) : (
127
- title
128
- )}
129
- {subtitle != null && (
130
- typeof subtitle === 'string' || typeof subtitle === 'number' ? (
131
- <ThemedText style={resolvedSubtitleStyle} numberOfLines={1}>{subtitle}</ThemedText>
129
+ >
130
+ <Avatar
131
+ {...resolvedAvatar}
132
+ size={avatarSize}
133
+ avatarStyle={(_, avatarState) => avatarTheme.container(
134
+ {
135
+ ...avatarState,
136
+ size: avatarSize,
137
+ },
138
+ resolvedAvatar.avatarStyle
139
+ )}
140
+ imageStyle={(_, avatarState) => avatarTheme.image(
141
+ {
142
+ ...avatarState,
143
+ size: avatarSize,
144
+ },
145
+ resolvedAvatar.imageStyle
146
+ )}
147
+ textStyle={(_, avatarState) => avatarTheme.text(
148
+ {
149
+ ...avatarState,
150
+ size: avatarSize,
151
+ },
152
+ resolvedAvatar.textStyle
153
+ )}
154
+ iconStyle={(_, avatarState) => avatarTheme.icon(
155
+ {
156
+ ...avatarState,
157
+ size: avatarSize,
158
+ },
159
+ resolvedAvatar.iconStyle
160
+ )}
161
+ />
162
+ <View style={resolvedContentRowStyle}>
163
+ {typeof title === 'string' || typeof title === 'number' ? (
164
+ <ThemedText style={resolvedTitleStyle} numberOfLines={1}>{title}</ThemedText>
132
165
  ) : (
133
- subtitle
134
- )
135
- )}
136
- </View>
166
+ title
167
+ )}
168
+ {subtitle != null && (
169
+ typeof subtitle === 'string' || typeof subtitle === 'number' ? (
170
+ <ThemedText style={resolvedSubtitleStyle} numberOfLines={1}>{subtitle}</ThemedText>
171
+ ) : (
172
+ subtitle
173
+ )
174
+ )}
175
+ </View>
176
+ </ThemedPressable>
137
177
  {rightActions != null && (
138
178
  <View style={{ flexDirection: 'row', alignItems: 'center', flexShrink: 0 }}>
139
179
  {rightActions}
@@ -12,23 +12,35 @@ import {
12
12
 
13
13
  import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
14
14
 
15
+ import { useDebugContext } from '../../global-contexts/debug'
15
16
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
16
- import { ThemedText } from '../visualization-and-display/ThemedText'
17
17
  import type {
18
+ ListActionItemDescriptionStyle,
18
19
  ListActionItemState,
19
20
  ListActionItemStyle,
20
21
  ListActionItemTitleStyle
21
22
  } from '../../theme/types/components/listItem'
22
23
  import type { StyleOverwrite } from '../../theme/types/resolver'
24
+ import { createHitBoxOverlayStyle } from '../../utils/hitBoxOverlay'
25
+ import { useMinimumTouchTargetHitSlop } from '../../utils/minimumTouchTargetHitSlop'
26
+ import { ListItemAccessory } from './ListItemAccessory'
27
+ import {
28
+ ListItemTextContent,
29
+ type ListItemContentOrder
30
+ } from './ListItemTextContent'
23
31
 
24
32
  export type ListActionItemProps = Omit<PressableProps, 'children' | 'style'> & {
25
- label: string,
33
+ title?: string,
34
+ subtitle?: string,
35
+ content?: ReactNode,
36
+ contentOrder?: ListItemContentOrder,
26
37
  leading?: ReactNode,
27
38
  trailing?: ReactNode,
28
39
  color?: ColorPairToken,
29
40
  style?: StyleProp<ViewStyle>,
30
41
  itemStyle?: StyleOverwrite<ListActionItemState, ListActionItemStyle>,
31
- labelStyle?: StyleOverwrite<ListActionItemState, ListActionItemTitleStyle>,
42
+ titleStyle?: StyleOverwrite<ListActionItemState, ListActionItemTitleStyle>,
43
+ subtitleStyle?: StyleOverwrite<ListActionItemState, ListActionItemDescriptionStyle>,
32
44
  }
33
45
 
34
46
  type PressableInteraction = {
@@ -39,17 +51,29 @@ type PressableInteraction = {
39
51
  }
40
52
 
41
53
  export const ListActionItem = ({
42
- label,
54
+ title,
55
+ subtitle,
56
+ content,
57
+ contentOrder = 'titleFirst',
43
58
  leading,
44
59
  trailing,
45
60
  color,
46
61
  disabled,
47
62
  style,
48
63
  itemStyle,
49
- labelStyle,
64
+ titleStyle,
65
+ subtitleStyle,
66
+ hitSlop: providedHitSlop,
67
+ onLayout: providedOnLayout,
50
68
  ...props
51
69
  }: ListActionItemProps) => {
52
70
  const { theme } = useTheme()
71
+ const { hitBox } = useDebugContext()
72
+ const { hitSlop, onLayout } = useMinimumTouchTargetHitSlop({
73
+ touchTargetSize: theme.semantics.touchTargetSize({}),
74
+ hitSlop: providedHitSlop,
75
+ onLayout: providedOnLayout,
76
+ })
53
77
 
54
78
  const resolveState = (interaction: PressableInteraction): ListActionItemState => ({
55
79
  color,
@@ -64,6 +88,8 @@ export const ListActionItem = ({
64
88
  <Pressable
65
89
  {...props}
66
90
  disabled={disabled}
91
+ hitSlop={hitSlop}
92
+ onLayout={onLayout}
67
93
  style={(pressableState) => {
68
94
  const state = resolveState(pressableState as PressableInteraction)
69
95
  return [theme.components.listItem.action.container(state, itemStyle), style]
@@ -74,22 +100,45 @@ export const ListActionItem = ({
74
100
  const resolvedLeadingItemContainerStyle = theme.components.listItem.action.leadingItemContainer(state)
75
101
  const resolvedContentStyle = theme.components.listItem.action.content(state)
76
102
  const resolvedTrailingItemContainerStyle = theme.components.listItem.action.trailingItemContainer(state)
77
- const resolvedLabelStyle = theme.components.listItem.action.titleText(state, labelStyle)
103
+ const resolvedTitleStyle = theme.components.listItem.action.titleText(state, titleStyle)
104
+ const resolvedSubtitleStyle = theme.components.listItem.action.descriptionText(state, subtitleStyle)
105
+ const resolvedIconStyle = theme.components.listItem.action.icon(state)
78
106
 
79
107
  return (
80
108
  <Fragment>
109
+ {hitBox.isVisualizing && (
110
+ <View
111
+ pointerEvents="none"
112
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
113
+ />
114
+ )}
81
115
  {leading != null && (
82
- <View style={resolvedLeadingItemContainerStyle}>
116
+ <ListItemAccessory
117
+ style={resolvedLeadingItemContainerStyle}
118
+ foreground={color?.onColor}
119
+ iconStyle={resolvedIconStyle}
120
+ >
83
121
  {leading}
84
- </View>
122
+ </ListItemAccessory>
85
123
  )}
86
124
  <View style={resolvedContentStyle}>
87
- <ThemedText style={resolvedLabelStyle}>{label}</ThemedText>
125
+ <ListItemTextContent
126
+ title={title}
127
+ subtitle={subtitle}
128
+ content={content}
129
+ contentOrder={contentOrder}
130
+ titleStyle={resolvedTitleStyle}
131
+ subtitleStyle={resolvedSubtitleStyle}
132
+ />
88
133
  </View>
89
134
  {trailing != null && (
90
- <View style={resolvedTrailingItemContainerStyle}>
135
+ <ListItemAccessory
136
+ style={resolvedTrailingItemContainerStyle}
137
+ foreground={color?.onColor}
138
+ iconStyle={resolvedIconStyle}
139
+ >
91
140
  {trailing}
92
- </View>
141
+ </ListItemAccessory>
93
142
  )}
94
143
  </Fragment>
95
144
  )