@helpwave/hightide-native 0.6.0 → 0.9.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 (88) hide show
  1. package/package.json +6 -8
  2. package/src/components/card/Card.tsx +2 -4
  3. package/src/components/chat/ChatAttachmentMessageBubble.tsx +152 -90
  4. package/src/components/chat/ChatConversationList.tsx +4 -12
  5. package/src/components/chat/ChatConversationRow.tsx +159 -164
  6. package/src/components/chat/ChatDateDivider.tsx +3 -8
  7. package/src/components/chat/ChatMessageBubble.tsx +52 -45
  8. package/src/components/chat/ChatMessageComposer.tsx +4 -12
  9. package/src/components/chat/ChatMessageList.tsx +3 -9
  10. package/src/components/chat/ChatQuickReplyChip.tsx +41 -27
  11. package/src/components/chat/ChatSystemLine.tsx +4 -12
  12. package/src/components/chat/ChatThreadHeader.tsx +136 -101
  13. package/src/components/layout/Divider.tsx +2 -4
  14. package/src/components/list/ListActionItem.tsx +57 -65
  15. package/src/components/list/ListItem.tsx +8 -28
  16. package/src/components/list/ListItemTextContent.tsx +4 -2
  17. package/src/components/list/ListNavigationItem.tsx +55 -63
  18. package/src/components/user-interaction/Button.tsx +43 -49
  19. package/src/components/user-interaction/Checkbox.tsx +47 -48
  20. package/src/components/user-interaction/IconButton.tsx +40 -42
  21. package/src/components/user-interaction/Input.tsx +69 -80
  22. package/src/components/user-interaction/MultiSelect/MultiSelect.tsx +17 -0
  23. package/src/components/user-interaction/MultiSelect/MultiSelectComponent.tsx +40 -0
  24. package/src/components/user-interaction/MultiSelect/MultiSelectContext.tsx +74 -0
  25. package/src/components/user-interaction/MultiSelect/MultiSelectMenu.tsx +87 -0
  26. package/src/components/user-interaction/MultiSelect/MultiSelectOption.tsx +106 -0
  27. package/src/components/user-interaction/MultiSelect/MultiSelectRoot.tsx +194 -0
  28. package/src/components/user-interaction/MultiSelect/MultiSelectTrigger.tsx +123 -0
  29. package/src/components/user-interaction/MultiSelect/index.ts +2 -0
  30. package/src/components/user-interaction/SearchBar.tsx +11 -17
  31. package/src/components/user-interaction/Select/Select.tsx +17 -0
  32. package/src/components/user-interaction/Select/SelectComponent.tsx +40 -0
  33. package/src/components/user-interaction/Select/SelectContext.tsx +73 -0
  34. package/src/components/user-interaction/Select/SelectMenu.tsx +87 -0
  35. package/src/components/user-interaction/Select/SelectOption.tsx +96 -0
  36. package/src/components/user-interaction/Select/SelectRoot.tsx +186 -0
  37. package/src/components/user-interaction/Select/SelectTrigger.tsx +92 -0
  38. package/src/components/user-interaction/Select/index.ts +2 -0
  39. package/src/components/user-interaction/Switch.tsx +46 -98
  40. package/src/components/user-interaction/Textarea.tsx +165 -12
  41. package/src/components/user-interaction/ThemedPressable.tsx +48 -49
  42. package/src/components/visualization-and-display/Avatar.tsx +19 -17
  43. package/src/components/visualization-and-display/Chip.tsx +4 -12
  44. package/src/components/visualization-and-display/ThemedText.tsx +12 -5
  45. package/src/enums/chatMessageDirection.ts +18 -0
  46. package/src/enums/chatMessageStatus.ts +18 -0
  47. package/src/enums/hightideThemeModes.ts +18 -0
  48. package/src/enums/index.ts +7 -0
  49. package/src/enums/listItemContentOrder.ts +18 -0
  50. package/src/enums/themedTextAppearance.ts +18 -0
  51. package/src/enums/useMultiSelectFirstHighlightBehavior.ts +18 -0
  52. package/src/enums/useSelectFirstHighlightBehavior.ts +18 -0
  53. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +9 -5
  54. package/src/global-contexts/theme/ThemeProvider.tsx +4 -1
  55. package/src/hooks/index.ts +2 -0
  56. package/src/hooks/useAnimatedStyleTransition.ts +460 -0
  57. package/src/hooks/useMemoizedTheme.ts +23 -0
  58. package/src/hooks/useMultiSelect.ts +3 -1
  59. package/src/hooks/useSelect.ts +3 -1
  60. package/src/icons/HightideIconRegistry.ts +2 -0
  61. package/src/theme/adapters/style-adapter-utils.ts +121 -27
  62. package/src/theme/resolvers/avatar.ts +48 -39
  63. package/src/theme/resolvers/button.ts +7 -17
  64. package/src/theme/resolvers/chat/attachment-message-bubble.ts +5 -6
  65. package/src/theme/resolvers/chat/conversation-row.ts +4 -6
  66. package/src/theme/resolvers/chat/message-bubble.ts +1 -2
  67. package/src/theme/resolvers/chat/message-composer.ts +1 -2
  68. package/src/theme/resolvers/chat/quick-reply-chip.ts +2 -3
  69. package/src/theme/resolvers/chat/system-line.ts +1 -2
  70. package/src/theme/resolvers/chat/thread-header.ts +3 -4
  71. package/src/theme/resolvers/checkbox.ts +2 -2
  72. package/src/theme/resolvers/chip.ts +1 -2
  73. package/src/theme/resolvers/iconButton.ts +4 -13
  74. package/src/theme/resolvers/input.ts +30 -14
  75. package/src/theme/resolvers/listItem.ts +2 -3
  76. package/src/theme/resolvers/multiSelect.ts +1 -2
  77. package/src/theme/resolvers/searchBar.ts +1 -2
  78. package/src/theme/resolvers/select.ts +1 -2
  79. package/src/theme/resolvers/themedPressable.ts +4 -15
  80. package/src/theme/themes/hightideThemes.ts +3 -1
  81. package/src/theme/types/components/chat.ts +6 -21
  82. package/src/theme/types/components/input.ts +3 -0
  83. package/src/theme/types/components/textarea.ts +1 -0
  84. package/src/theme/types/resolver.ts +4 -27
  85. package/src/utils/index.ts +1 -0
  86. package/src/utils/pressableInteraction.ts +7 -0
  87. package/src/components/user-interaction/MultiSelect.tsx +0 -282
  88. package/src/components/user-interaction/Select.tsx +0 -242
@@ -1,282 +0,0 @@
1
- import { Fragment, useMemo } from 'react'
2
- import {
3
- FlatList,
4
- Modal,
5
- Pressable,
6
- View,
7
- type StyleProp,
8
- type ViewStyle
9
- } from 'react-native'
10
- import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
11
- import { Chip } from '../visualization-and-display/Chip'
12
- import { ThemedText } from '../visualization-and-display/ThemedText'
13
- import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
14
- import { ListActionItem } from '../list/ListActionItem'
15
- import { IconButton } from './IconButton'
16
- import { SearchBar } from './SearchBar'
17
- import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
18
- import { useTheme } from '../../global-contexts/theme/ThemeContext'
19
- import {
20
- useMultiSelect,
21
- type UseMultiSelectOption
22
- } from '../../hooks/useMultiSelect'
23
- import type { MultiSelectState } from '../../theme/types/components/multiSelect'
24
- import type {
25
- FormFieldDataHandling,
26
- FormFieldInteractionStates
27
- } from '../../types/formField'
28
- import { useTranslation } from '@helpwave/hightide-utils/context'
29
-
30
- export type MultiSelectOption = UseMultiSelectOption
31
-
32
- export type MultiSelectProps = Partial<FormFieldDataHandling<string[]>>
33
- & Partial<FormFieldInteractionStates>
34
- & {
35
- options: ReadonlyArray<MultiSelectOption>,
36
- value?: string[],
37
- initialValue?: string[],
38
- placeholder?: string,
39
- searchableThreshold?: number,
40
- color?: ColorPairToken,
41
- style?: StyleProp<ViewStyle>,
42
- }
43
-
44
- export const MultiSelect = ({
45
- options,
46
- value: controlledValue,
47
- initialValue = [],
48
- placeholder = 'Select…',
49
- searchableThreshold = 6,
50
- color,
51
- disabled = false,
52
- readOnly = false,
53
- invalid = false,
54
- onValueChange,
55
- onEditComplete,
56
- style,
57
- }: MultiSelectProps) => {
58
- const { theme } = useTheme()
59
- const translation = useTranslation()
60
- const interactive = !disabled && !readOnly
61
-
62
- const multiSelect = useMultiSelect({
63
- options,
64
- value: controlledValue,
65
- initialValue,
66
- onValueChange,
67
- onEditComplete,
68
- })
69
-
70
- const isSearchVisible = options.length >= searchableThreshold
71
-
72
- const selectedOptions = useMemo(() => {
73
- return options.filter((option) => multiSelect.isSelected(option.id))
74
- }, [multiSelect, options])
75
-
76
- const state = useMemo((): MultiSelectState => ({
77
- color,
78
- isDisabled: disabled,
79
- isReadonly: readOnly,
80
- isInvalid: invalid,
81
- isOpen: multiSelect.isOpen,
82
- hasSelections: selectedOptions.length > 0,
83
- hasValue: selectedOptions.length > 0,
84
- }), [color, disabled, invalid, multiSelect.isOpen, readOnly, selectedOptions.length])
85
-
86
- const multiSelectTheme = theme.components.multiSelect
87
-
88
- const resolvedOverlayStyle = useMemo(
89
- () => multiSelectTheme.overlay({}),
90
- [multiSelectTheme]
91
- )
92
- const resolvedMenuStyle = useMemo(
93
- () => multiSelectTheme.menu({ hasSearch: isSearchVisible }),
94
- [multiSelectTheme, isSearchVisible]
95
- )
96
- const resolvedHeaderStyle = useMemo(
97
- () => multiSelectTheme.header({}),
98
- [multiSelectTheme]
99
- )
100
- const resolvedEmptyTextStyle = useMemo(
101
- () => multiSelectTheme.emptyText({}),
102
- [multiSelectTheme]
103
- )
104
- const visibleOptions = useMemo(
105
- () => options.filter((option) => multiSelect.visibleOptionIds.includes(option.id)),
106
- [options, multiSelect.visibleOptionIds]
107
- )
108
- const showEmptySearchResults = isSearchVisible
109
- && multiSelect.searchQuery.trim().length > 0
110
- && visibleOptions.length === 0
111
-
112
- return (
113
- <View style={[{ width: '100%' }, style]}>
114
- <Pressable
115
- disabled={!interactive}
116
- onPress={() => multiSelect.toggleOpen()}
117
- style={(pressableState) => {
118
- const interaction = pressableState as {
119
- pressed: boolean,
120
- hovered?: boolean,
121
- focused?: boolean,
122
- focusVisible?: boolean,
123
- }
124
- return multiSelectTheme.trigger({
125
- ...state,
126
- isPressed: interaction.pressed,
127
- isHovered: !!interaction.hovered,
128
- isFocused: !!interaction.focused,
129
- isFocusVisible: !!interaction.focusVisible,
130
- })
131
- }}
132
- >
133
- {(pressableState) => {
134
- const interaction = pressableState as {
135
- pressed: boolean,
136
- hovered?: boolean,
137
- focused?: boolean,
138
- focusVisible?: boolean,
139
- }
140
- const triggerState = {
141
- ...state,
142
- isPressed: interaction.pressed,
143
- isHovered: !!interaction.hovered,
144
- isFocused: !!interaction.focused,
145
- isFocusVisible: !!interaction.focusVisible,
146
- }
147
-
148
- return (
149
- <Fragment>
150
- <View
151
- pointerEvents="none"
152
- style={multiSelectTheme.stateLayer(triggerState)}
153
- />
154
- {selectedOptions.length > 0
155
- ? (
156
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, flex: 1 }}>
157
- {selectedOptions.map((option) => (
158
- <Chip key={option.id} size="md" color={color} variant="tonal">
159
- <ThemedText>{option.label ?? option.id}</ThemedText>
160
- {!state.isReadonly && (
161
- <View
162
- style={{
163
- position: 'relative',
164
- width: theme.icongraphy.sizes.sm,
165
- height: theme.icongraphy.sizes.sm,
166
- }}
167
- >
168
- <IconButton
169
- accessibilityLabel={translation('remove')}
170
- size="sm"
171
- color={theme.colors.negative}
172
- variant="foreground"
173
- disabled={!interactive}
174
- onPress={() => multiSelect.toggleSelection(option.id, false)}
175
- style={{
176
- position: 'absolute',
177
- left: '50%',
178
- top: '50%',
179
- transform: [
180
- { translateX: '-50%' },
181
- { translateY: '-50%' },
182
- ],
183
- }}
184
- icon={HightideIconRegistry.X}
185
- />
186
- </View>
187
- )}
188
- </Chip>
189
- ))}
190
- </View>
191
- )
192
- : (
193
- <ThemedText style={multiSelectTheme.triggerText(triggerState)}>
194
- {placeholder}
195
- </ThemedText>
196
- )}
197
- </Fragment>
198
- )
199
- }}
200
- </Pressable>
201
-
202
- <Modal
203
- visible={multiSelect.isOpen}
204
- transparent
205
- animationType="fade"
206
- onRequestClose={() => multiSelect.setIsOpen(false)}
207
- >
208
- <Pressable
209
- style={resolvedOverlayStyle}
210
- onPress={() => multiSelect.setIsOpen(false)}
211
- >
212
- <Pressable
213
- style={resolvedMenuStyle}
214
- onPress={(event) => event.stopPropagation()}
215
- >
216
- {isSearchVisible && (
217
- <View style={resolvedHeaderStyle}>
218
- <SearchBar
219
- value={multiSelect.searchQuery}
220
- onValueChange={multiSelect.setSearchQuery}
221
- onSearch={multiSelect.setSearchQuery}
222
- />
223
- </View>
224
- )}
225
- {showEmptySearchResults ? (
226
- <View
227
- style={{
228
- flex: 1,
229
- justifyContent: 'center',
230
- alignItems: 'center',
231
- }}
232
- >
233
- <ThemedText style={resolvedEmptyTextStyle}>
234
- {translation('nothingFound')}
235
- </ThemedText>
236
- </View>
237
- ) : (
238
- <FlatList
239
- data={visibleOptions}
240
- keyExtractor={(option) => option.id}
241
- style={{ flex: 1 }}
242
- renderItem={({ item }) => {
243
- const selected = multiSelect.isSelected(item.id)
244
- const isHighlighted = multiSelect.highlightedId === item.id
245
- const optionState = {
246
- color,
247
- isSelected: selected,
248
- isHighlighted,
249
- isDisabled: item.disabled,
250
- }
251
- const checkboxIcon = multiSelectTheme.checkboxIcon(optionState)
252
- const optionColor = selected
253
- ? (color ?? theme.colors.primary)
254
- : undefined
255
-
256
- return (
257
- <ListActionItem
258
- title={item.label ?? item.id}
259
- color={optionColor}
260
- disabled={item.disabled}
261
- onPress={() => multiSelect.toggleSelection(item.id)}
262
- leading={(
263
- <View style={multiSelectTheme.checkbox(optionState)}>
264
- <ThemedIcon
265
- icon={HightideIconRegistry.Check}
266
- size={checkboxIcon.size}
267
- strokeWidth={checkboxIcon.strokeWidth}
268
- color={checkboxIcon.color}
269
- />
270
- </View>
271
- )}
272
- />
273
- )
274
- }}
275
- />
276
- )}
277
- </Pressable>
278
- </Pressable>
279
- </Modal>
280
- </View>
281
- )
282
- }
@@ -1,242 +0,0 @@
1
- import { Fragment, useMemo } from 'react'
2
- import {
3
- FlatList,
4
- Modal,
5
- Pressable,
6
- View,
7
- type StyleProp,
8
- type ViewStyle
9
- } from 'react-native'
10
-
11
- import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
12
- import { useTranslation } from '@helpwave/hightide-utils/context'
13
- import { useTheme } from '../../global-contexts/theme/ThemeContext'
14
- import { ThemedText } from '../visualization-and-display/ThemedText'
15
- import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
16
- import { ListActionItem } from '../list/ListActionItem'
17
- import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
18
- import {
19
- useSelect,
20
- type UseSelectOption
21
- } from '../../hooks/useSelect'
22
- import type { SelectState } from '../../theme/types/components/select'
23
- import type {
24
- FormFieldDataHandling,
25
- FormFieldInteractionStates
26
- } from '../../types/formField'
27
- import { SearchBar } from './SearchBar'
28
-
29
- export type SelectOption<T extends string = string> = UseSelectOption & {
30
- value?: T,
31
- }
32
-
33
- export type SelectProps = Partial<FormFieldDataHandling<string>>
34
- & Partial<FormFieldInteractionStates>
35
- & {
36
- options: ReadonlyArray<SelectOption>,
37
- value?: string | null,
38
- initialValue?: string | null,
39
- placeholder?: string,
40
- searchableThreshold?: number,
41
- color?: ColorPairToken,
42
- style?: StyleProp<ViewStyle>,
43
- }
44
-
45
- export const Select = ({
46
- options,
47
- value: controlledValue,
48
- initialValue = null,
49
- placeholder = 'Select…',
50
- searchableThreshold = 6,
51
- color,
52
- disabled = false,
53
- readOnly = false,
54
- invalid = false,
55
- onValueChange,
56
- onEditComplete,
57
- style,
58
- }: SelectProps) => {
59
- const { theme } = useTheme()
60
- const translation = useTranslation()
61
- const interactive = !disabled && !readOnly
62
-
63
- const select = useSelect({
64
- options,
65
- value: controlledValue,
66
- initialValue,
67
- onValueChange,
68
- onEditComplete,
69
- })
70
-
71
- const isSearchVisible = options.length >= searchableThreshold
72
-
73
- const selectedLabel = useMemo(() => {
74
- const selected = options.find((option) => option.id === select.value)
75
- return selected?.label ?? placeholder
76
- }, [options, placeholder, select.value])
77
-
78
- const visibleOptions = useMemo(
79
- () => options.filter((option) => select.visibleOptionIds.includes(option.id)),
80
- [options, select.visibleOptionIds]
81
- )
82
-
83
- const state = useMemo((): SelectState => ({
84
- color,
85
- isDisabled: disabled,
86
- isReadonly: readOnly,
87
- isInvalid: invalid,
88
- isOpen: select.isOpen,
89
- hasValue: !!select.value,
90
- }), [color, disabled, invalid, readOnly, select.isOpen, select.value])
91
-
92
- const selectTheme = theme.components.select
93
-
94
- const resolvedOverlayStyle = useMemo(
95
- () => selectTheme.overlay({}),
96
- [selectTheme]
97
- )
98
- const resolvedMenuStyle = useMemo(
99
- () => selectTheme.menu({ hasSearch: isSearchVisible }),
100
- [selectTheme, isSearchVisible]
101
- )
102
- const resolvedHeaderStyle = useMemo(
103
- () => selectTheme.header({}),
104
- [selectTheme]
105
- )
106
- const resolvedEmptyTextStyle = useMemo(
107
- () => selectTheme.emptyText({}),
108
- [selectTheme]
109
- )
110
- const showEmptySearchResults = isSearchVisible
111
- && select.searchQuery.trim().length > 0
112
- && visibleOptions.length === 0
113
-
114
- return (
115
- <View style={style}>
116
- <Pressable
117
- disabled={!interactive}
118
- onPress={() => select.toggleOpen()}
119
- style={(pressableState) => {
120
- const interaction = pressableState as {
121
- pressed: boolean,
122
- hovered?: boolean,
123
- focused?: boolean,
124
- focusVisible?: boolean,
125
- }
126
- return selectTheme.trigger({
127
- ...state,
128
- isPressed: interaction.pressed,
129
- isHovered: !!interaction.hovered,
130
- isFocused: !!interaction.focused,
131
- isFocusVisible: !!interaction.focusVisible,
132
- })
133
- }}
134
- >
135
- {(pressableState) => {
136
- const interaction = pressableState as {
137
- pressed: boolean,
138
- hovered?: boolean,
139
- focused?: boolean,
140
- focusVisible?: boolean,
141
- }
142
- const triggerState = {
143
- ...state,
144
- isPressed: interaction.pressed,
145
- isHovered: !!interaction.hovered,
146
- isFocused: !!interaction.focused,
147
- isFocusVisible: !!interaction.focusVisible,
148
- }
149
- const icon = selectTheme.icon(triggerState)
150
-
151
- return (
152
- <Fragment>
153
- <View
154
- pointerEvents="none"
155
- style={selectTheme.stateLayer(triggerState)}
156
- />
157
- <ThemedText style={[selectTheme.triggerText(triggerState), { flex: 1 }]}>
158
- {selectedLabel}
159
- </ThemedText>
160
- <ThemedIcon
161
- icon={HightideIconRegistry.ChevronDown}
162
- size={icon.size}
163
- strokeWidth={icon.strokeWidth}
164
- color={icon.color}
165
- />
166
- </Fragment>
167
- )
168
- }}
169
- </Pressable>
170
-
171
- <Modal
172
- visible={select.isOpen}
173
- transparent
174
- animationType="fade"
175
- onRequestClose={() => select.setIsOpen(false)}
176
- >
177
- <Pressable
178
- style={resolvedOverlayStyle}
179
- onPress={() => select.setIsOpen(false)}
180
- >
181
- <Pressable
182
- style={resolvedMenuStyle}
183
- onPress={(event) => event.stopPropagation()}
184
- >
185
- {isSearchVisible && (
186
- <View style={resolvedHeaderStyle}>
187
- <SearchBar
188
- value={select.searchQuery}
189
- onValueChange={select.setSearchQuery}
190
- onSearch={select.setSearchQuery}
191
- />
192
- </View>
193
- )}
194
- {showEmptySearchResults ? (
195
- <View
196
- style={{
197
- flex: 1,
198
- justifyContent: 'center',
199
- alignItems: 'center',
200
- }}
201
- >
202
- <ThemedText style={resolvedEmptyTextStyle}>
203
- {translation('nothingFound')}
204
- </ThemedText>
205
- </View>
206
- ) : (
207
- <FlatList
208
- data={visibleOptions}
209
- keyExtractor={(option) => option.id}
210
- style={{ flex: 1 }}
211
- renderItem={({ item }) => {
212
- const isSelected = select.value === item.id
213
- const optionColor = isSelected
214
- ? (color ?? theme.colors.primary)
215
- : undefined
216
- const checkIcon = theme.components.listItem.action.icon({ color: optionColor })
217
-
218
- return (
219
- <ListActionItem
220
- title={item.label ?? item.id}
221
- color={optionColor}
222
- disabled={item.disabled}
223
- onPress={() => select.selectValue(item.id)}
224
- leading={(
225
- <ThemedIcon
226
- icon={HightideIconRegistry.Check}
227
- size={checkIcon.size}
228
- strokeWidth={checkIcon.strokeWidth}
229
- color={isSelected ? theme.colors.primary.color : 'transparent'}
230
- />
231
- )}
232
- />
233
- )
234
- }}
235
- />
236
- )}
237
- </Pressable>
238
- </Pressable>
239
- </Modal>
240
- </View>
241
- )
242
- }