@helpwave/hightide-native 0.0.8 → 0.1.1

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 (125) hide show
  1. package/README.md +4 -4
  2. package/package.json +8 -19
  3. package/src/components/card/Card.tsx +41 -0
  4. package/src/components/card/index.ts +1 -0
  5. package/src/components/chat/ChatAttachmentMessageBubble.tsx +246 -0
  6. package/src/components/chat/ChatConversationRow.tsx +21 -16
  7. package/src/components/chat/ChatDateDivider.tsx +2 -2
  8. package/src/components/chat/ChatMessageBubble.tsx +62 -52
  9. package/src/components/chat/ChatMessageComposer.tsx +16 -14
  10. package/src/components/chat/ChatQuickReplyChip.tsx +41 -43
  11. package/src/components/chat/ChatSystemLine.tsx +14 -7
  12. package/src/components/chat/ChatThreadHeader.tsx +60 -6
  13. package/src/components/chat/index.ts +1 -2
  14. package/src/components/index.ts +3 -1
  15. package/src/components/layout/Divider.tsx +60 -0
  16. package/src/components/layout/index.ts +1 -0
  17. package/src/components/list/ListActionItem.tsx +99 -0
  18. package/src/components/list/ListItem.tsx +96 -0
  19. package/src/components/list/ListNavigationItem.tsx +102 -0
  20. package/src/components/list/index.ts +3 -0
  21. package/src/components/user-interaction/Button.tsx +80 -31
  22. package/src/components/user-interaction/Checkbox.tsx +72 -32
  23. package/src/components/user-interaction/IconButton.tsx +68 -30
  24. package/src/components/user-interaction/Input.tsx +61 -14
  25. package/src/components/user-interaction/MultiSelect.tsx +142 -75
  26. package/src/components/user-interaction/SearchBar.tsx +222 -0
  27. package/src/components/user-interaction/Select.tsx +129 -58
  28. package/src/components/user-interaction/Switch.tsx +110 -80
  29. package/src/components/user-interaction/ThemedPressable.tsx +136 -0
  30. package/src/components/user-interaction/index.ts +2 -0
  31. package/src/components/visualization-and-display/Avatar.tsx +112 -93
  32. package/src/components/visualization-and-display/Chip.tsx +22 -22
  33. package/src/components/visualization-and-display/ThemedIcon.tsx +72 -0
  34. package/src/components/visualization-and-display/ThemedText.tsx +38 -0
  35. package/src/components/visualization-and-display/index.ts +2 -1
  36. package/src/global-contexts/HightideProvider.tsx +19 -10
  37. package/src/global-contexts/content-theme/ContentThemeContext.ts +22 -0
  38. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +32 -0
  39. package/src/global-contexts/content-theme/index.ts +2 -0
  40. package/src/global-contexts/debug/forward-exports.ts +10 -0
  41. package/src/global-contexts/debug/index.ts +1 -0
  42. package/src/global-contexts/index.ts +2 -0
  43. package/src/global-contexts/theme/ThemeProvider.tsx +7 -1
  44. package/src/icons/HightideIconRegistry.ts +33 -0
  45. package/src/icons/index.ts +2 -0
  46. package/src/icons/types.ts +11 -0
  47. package/src/theme/adapters/container-adapter.ts +323 -0
  48. package/src/theme/adapters/defined.ts +11 -0
  49. package/src/theme/adapters/icon-style-adapter.ts +10 -0
  50. package/src/theme/adapters/index.ts +4 -0
  51. package/src/theme/adapters/text-style-adapter.ts +16 -0
  52. package/src/theme/index.ts +1 -0
  53. package/src/theme/resolvers/avatar.ts +385 -186
  54. package/src/theme/resolvers/button.ts +50 -82
  55. package/src/theme/resolvers/card.ts +27 -0
  56. package/src/theme/resolvers/chat/attachment-message-bubble.ts +163 -0
  57. package/src/theme/resolvers/chat/chat-theme.ts +69 -0
  58. package/src/theme/resolvers/chat/conversation-list.ts +35 -0
  59. package/src/theme/resolvers/chat/conversation-row.ts +64 -0
  60. package/src/theme/resolvers/chat/date-divider.ts +31 -0
  61. package/src/theme/resolvers/chat/index.ts +11 -0
  62. package/src/theme/resolvers/chat/message-bubble.ts +57 -0
  63. package/src/theme/resolvers/chat/message-composer.ts +38 -0
  64. package/src/theme/resolvers/chat/message-list.ts +27 -0
  65. package/src/theme/resolvers/chat/quick-reply-chip.ts +80 -0
  66. package/src/theme/resolvers/chat/system-line.ts +41 -0
  67. package/src/theme/resolvers/chat/thread-header.ts +85 -0
  68. package/src/theme/resolvers/checkbox.ts +87 -74
  69. package/src/theme/resolvers/chip.ts +30 -67
  70. package/src/theme/resolvers/colorScheme.ts +136 -0
  71. package/src/theme/resolvers/divider.ts +34 -0
  72. package/src/theme/resolvers/icon.ts +21 -0
  73. package/src/theme/resolvers/iconButton.ts +45 -61
  74. package/src/theme/resolvers/index.ts +7 -2
  75. package/src/theme/resolvers/input.ts +64 -42
  76. package/src/theme/resolvers/listItem.ts +115 -0
  77. package/src/theme/resolvers/multiSelect.ts +159 -84
  78. package/src/theme/resolvers/searchBar.ts +121 -0
  79. package/src/theme/resolvers/select.ts +141 -66
  80. package/src/theme/resolvers/switch.ts +59 -53
  81. package/src/theme/resolvers/themedPressable.ts +54 -0
  82. package/src/theme/themes/createHightideTheme.ts +214 -51
  83. package/src/theme/themes/hightideThemes.ts +10 -6
  84. package/src/theme/types/color.ts +2 -59
  85. package/src/theme/types/components/avatar.ts +37 -36
  86. package/src/theme/types/components/button.ts +13 -14
  87. package/src/theme/types/components/card.ts +9 -0
  88. package/src/theme/types/components/chat.ts +124 -103
  89. package/src/theme/types/components/checkbox.ts +11 -12
  90. package/src/theme/types/components/chip.ts +8 -14
  91. package/src/theme/types/components/divider.ts +18 -0
  92. package/src/theme/types/components/hightide.ts +41 -42
  93. package/src/theme/types/components/iconButton.ts +19 -14
  94. package/src/theme/types/components/index.ts +5 -1
  95. package/src/theme/types/components/input.ts +17 -8
  96. package/src/theme/types/components/listItem.ts +86 -0
  97. package/src/theme/types/components/multiSelect.ts +14 -12
  98. package/src/theme/types/components/searchBar.ts +28 -0
  99. package/src/theme/types/components/select.ts +21 -9
  100. package/src/theme/types/components/switch.ts +12 -5
  101. package/src/theme/types/components/themedPressable.ts +31 -0
  102. package/src/theme/types/decoration.ts +1 -11
  103. package/src/theme/types/icongraphy.ts +8 -0
  104. package/src/theme/types/index.ts +2 -0
  105. package/src/theme/types/layout.ts +25 -57
  106. package/src/theme/types/resolver.ts +54 -1
  107. package/src/theme/types/semantics.ts +72 -0
  108. package/src/theme/types/theme.ts +26 -13
  109. package/src/theme/types/typography.ts +16 -17
  110. package/src/utils/hitBoxOverlay.ts +44 -0
  111. package/src/utils/index.ts +2 -0
  112. package/src/utils/minimumTouchTargetHitSlop.ts +68 -0
  113. package/src/components/chat/ChatAttachmentCard.tsx +0 -110
  114. package/src/components/chat/ChatMessageCard.tsx +0 -118
  115. package/src/components/menu/Menu.tsx +0 -61
  116. package/src/components/menu/MenuActionItem.tsx +0 -90
  117. package/src/components/menu/MenuItem.tsx +0 -73
  118. package/src/components/menu/MenuNavigationItem.tsx +0 -90
  119. package/src/components/menu/index.ts +0 -4
  120. package/src/components/visualization-and-display/Icon.tsx +0 -27
  121. package/src/storybook/helper.tsx +0 -36
  122. package/src/theme/resolvers/chat.ts +0 -418
  123. package/src/theme/resolvers/coloring.ts +0 -94
  124. package/src/theme/resolvers/menu.ts +0 -120
  125. package/src/theme/types/components/menu.ts +0 -57
@@ -1,72 +1,88 @@
1
- import {
2
- forwardRef,
3
- type ReactNode
4
- } from 'react'
1
+ import { Fragment, forwardRef } from 'react'
5
2
  import {
6
3
  Pressable,
4
+ View,
7
5
  type PressableProps,
8
6
  type StyleProp,
9
7
  type ViewStyle
10
8
  } from 'react-native'
11
- import type { LucideIcon } from 'lucide-react-native'
12
-
13
- import type { ColoringType } from '@helpwave/hightide-design/utils'
9
+ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
14
10
  import type {
15
- ButtonColoringStyle,
16
- ElementSize
17
- } from '@helpwave/hightide-design/types'
11
+ ComponentSize,
12
+ IconButtonVariant
13
+ } from '@helpwave/hightide-design/semantic-token-resolvers'
18
14
 
19
- import { Icon } from '../visualization-and-display/Icon'
15
+ import { ContentThemeProvider } from '../../global-contexts/content-theme/ContentThemeProvider'
16
+ import { useDebugContext } from '../../global-contexts/debug'
20
17
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
21
18
  import type {
22
19
  IconButtonState,
23
20
  IconButtonStyle
24
21
  } from '../../theme/types/components/iconButton'
25
22
  import type { StyleOverwrite } from '../../theme/types/resolver'
23
+ import type { IconComponent } from '../../icons'
24
+ import { createHitBoxOverlayStyle } from '../../utils/hitBoxOverlay'
25
+ import { useMinimumTouchTargetHitSlop } from '../../utils/minimumTouchTargetHitSlop'
26
+ import { ThemedIcon } from '../visualization-and-display'
27
+
28
+ export type IconButtonSize = ComponentSize
26
29
 
27
- export type IconButtonSize = ElementSize
30
+ export type { IconButtonVariant }
31
+
32
+ export const IconButtonUtil = {
33
+ sizes: ['xs', 'sm', 'md', 'lg', 'xl'] as const satisfies readonly ComponentSize[],
34
+ variants: ['elevated', 'filled', 'tonal', 'foreground'] as const satisfies readonly IconButtonVariant[],
35
+ }
28
36
 
29
37
  export type IconButtonProps = Omit<PressableProps, 'children' | 'style'> & {
30
38
  size?: IconButtonSize,
31
- color?: ColoringType,
32
- coloringStyle?: ButtonColoringStyle,
33
- icon?: LucideIcon,
34
- iconSize?: ElementSize,
35
- children?: ReactNode,
39
+ color?: ColorPairToken,
40
+ variant?: IconButtonVariant,
41
+ icon: IconComponent,
36
42
  accessibilityLabel: string,
37
43
  style?: StyleProp<ViewStyle>,
38
- buttonStyle?: StyleOverwrite<IconButtonState, IconButtonStyle>,
44
+ containerStyle?: StyleOverwrite<IconButtonState, IconButtonStyle>,
45
+ stateLayerStyle?: StyleOverwrite<IconButtonState, IconButtonStyle>,
39
46
  }
40
47
 
41
48
  type PressableInteraction = {
42
49
  pressed: boolean,
43
50
  hovered?: boolean,
44
51
  focused?: boolean,
52
+ focusVisible?: boolean,
45
53
  }
46
54
 
47
55
  export const IconButton = forwardRef<React.ComponentRef<typeof Pressable>, IconButtonProps>(function IconButton({
48
- children,
49
- icon,
50
- iconSize,
56
+ icon: IconComponent,
51
57
  size = 'md',
52
- color = 'neutral',
53
- coloringStyle = 'solid',
58
+ color,
59
+ variant = 'filled',
54
60
  disabled,
55
61
  accessibilityLabel,
56
62
  style,
57
- buttonStyle,
63
+ containerStyle,
64
+ stateLayerStyle,
65
+ hitSlop: providedHitSlop,
66
+ onLayout: providedOnLayout,
58
67
  ...props
59
68
  }, ref) {
60
69
  const { theme } = useTheme()
70
+ const { hitBox } = useDebugContext()
71
+ const { hitSlop, onLayout } = useMinimumTouchTargetHitSlop({
72
+ touchTargetSize: theme.semantics.touchTargetSize({}),
73
+ hitSlop: providedHitSlop,
74
+ onLayout: providedOnLayout,
75
+ })
61
76
 
62
77
  const resolveState = (interaction: PressableInteraction): IconButtonState => ({
63
78
  size,
64
79
  color,
65
- coloringStyle,
80
+ variant,
66
81
  isDisabled: !!disabled,
67
82
  isPressed: interaction.pressed,
68
83
  isHovered: !!interaction.hovered,
69
84
  isFocused: !!interaction.focused,
85
+ isFocusVisible: !!interaction.focusVisible,
70
86
  })
71
87
 
72
88
  return (
@@ -76,19 +92,41 @@ export const IconButton = forwardRef<React.ComponentRef<typeof Pressable>, IconB
76
92
  disabled={disabled}
77
93
  accessibilityRole="button"
78
94
  accessibilityLabel={accessibilityLabel}
95
+ hitSlop={hitSlop}
96
+ onLayout={onLayout}
79
97
  style={(pressableState) => {
80
98
  const state = resolveState(pressableState as PressableInteraction)
81
- return [theme.components.iconButton.button(state, buttonStyle), style]
99
+ return [
100
+ theme.components.iconButton.container(state, containerStyle),
101
+ style,
102
+ ]
82
103
  }}
83
104
  >
84
105
  {(pressableState) => {
85
106
  const state = resolveState(pressableState as PressableInteraction)
86
107
  const resolvedIcon = theme.components.iconButton.icon(state)
108
+ const resolvedText = theme.components.iconButton.text(state)
87
109
 
88
- if (icon) {
89
- return <Icon icon={icon} size={iconSize ?? size} color={resolvedIcon.color} />
90
- }
91
- return children
110
+ return (
111
+ <Fragment>
112
+ {hitBox.isVisualizing && (
113
+ <View
114
+ pointerEvents="none"
115
+ style={createHitBoxOverlayStyle(hitSlop, hitBox.color)}
116
+ />
117
+ )}
118
+ <View
119
+ pointerEvents="none"
120
+ style={theme.components.iconButton.stateLayer(state, stateLayerStyle)}
121
+ />
122
+ <ContentThemeProvider
123
+ foregroundColor={resolvedIcon.color ?? theme.colors.primary.color}
124
+ textStyle={resolvedText}
125
+ >
126
+ <ThemedIcon icon={IconComponent} size={resolvedIcon.size} strokeWidth={resolvedIcon.strokeWidth}/>
127
+ </ContentThemeProvider>
128
+ </Fragment>
129
+ )
92
130
  }}
93
131
  </Pressable>
94
132
  )
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  forwardRef,
3
- useMemo
3
+ useMemo,
4
+ useState
4
5
  } from 'react'
5
6
  import {
6
7
  TextInput,
@@ -15,10 +16,12 @@ import {
15
16
  type UseDelayOptionsResolved
16
17
  } from '@helpwave/hightide-utils/hooks'
17
18
 
19
+ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
18
20
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
19
21
  import type {
22
+ InputContainerStyle,
20
23
  InputState,
21
- InputStyle
24
+ InputTextStyle
22
25
  } from '../../theme/types/components/input'
23
26
  import type { StyleOverwrite } from '../../theme/types/resolver'
24
27
  import type {
@@ -45,15 +48,18 @@ export type InputProps = Omit<TextInputProps, 'value' | 'style'>
45
48
  & Partial<FormFieldDataHandling<string>>
46
49
  & Partial<FormFieldInteractionStates>
47
50
  & {
51
+ color?: ColorPairToken,
48
52
  editCompleteOptions?: EditCompleteOptions,
49
53
  initialValue?: string,
50
54
  style?: StyleProp<TextStyle>,
51
- inputStyle?: StyleOverwrite<InputState, InputStyle>,
55
+ containerStyle?: StyleOverwrite<InputState, InputContainerStyle>,
56
+ textStyle?: StyleOverwrite<InputState, InputTextStyle>,
52
57
  }
53
58
 
54
59
  export const Input = forwardRef<TextInput, InputProps>(function Input({
55
60
  value: controlledValue,
56
61
  initialValue,
62
+ color,
57
63
  invalid = false,
58
64
  disabled = false,
59
65
  readOnly = false,
@@ -62,10 +68,14 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
62
68
  onEditComplete,
63
69
  editCompleteOptions,
64
70
  style,
65
- inputStyle,
71
+ containerStyle,
72
+ textStyle,
66
73
  ...props
67
74
  }, ref) {
68
75
  const { theme } = useTheme()
76
+ const [isHovered, setIsHovered] = useState(false)
77
+ const [isPressed, setIsPressed] = useState(false)
78
+ const [isFocused, setIsFocused] = useState(false)
69
79
 
70
80
  const [value, setValue] = useControlledState({
71
81
  value: controlledValue,
@@ -85,18 +95,28 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
85
95
  disabled: !afterDelay || disabled || readOnly,
86
96
  })
87
97
 
98
+ const interactive = !disabled && !readOnly
99
+
88
100
  const state = useMemo((): InputState => ({
101
+ color,
89
102
  isDisabled: disabled,
90
103
  isInvalid: invalid,
91
- isReadOnly: readOnly,
92
- }), [disabled, invalid, readOnly])
104
+ isReadonly: readOnly,
105
+ isHovered: interactive && isHovered,
106
+ isPressed: interactive && isPressed,
107
+ isFocused: interactive && isFocused,
108
+ }), [color, disabled, invalid, readOnly, interactive, isHovered, isPressed, isFocused])
93
109
 
94
- const resolvedInputStyle = useMemo(
95
- () => theme.components.input.input(state, inputStyle),
96
- [theme, state, inputStyle]
110
+ const resolvedContainerStyle = useMemo(
111
+ () => theme.components.input.container(state, containerStyle),
112
+ [theme, state, containerStyle]
113
+ )
114
+ const resolvedTextStyle = useMemo(
115
+ () => theme.components.input.text(state, textStyle),
116
+ [theme, state, textStyle]
97
117
  )
98
- const placeholderColor = useMemo(
99
- () => theme.components.input.placeholderColor(state),
118
+ const resolvedPlaceholderStyle = useMemo(
119
+ () => theme.components.input.placeholder(state),
100
120
  [theme, state]
101
121
  )
102
122
 
@@ -105,7 +125,7 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
105
125
  {...props}
106
126
  ref={ref}
107
127
  value={value}
108
- editable={!disabled && !readOnly}
128
+ editable={interactive}
109
129
  onChangeText={(nextValue) => {
110
130
  props.onChangeText?.(nextValue)
111
131
  restartTimer(() => {
@@ -113,13 +133,40 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
113
133
  })
114
134
  setValue(nextValue)
115
135
  }}
136
+ onFocus={(event) => {
137
+ if (interactive) {
138
+ setIsFocused(true)
139
+ }
140
+ props.onFocus?.(event)
141
+ }}
116
142
  onBlur={(event) => {
143
+ setIsFocused(false)
117
144
  props.onBlur?.(event)
118
145
  if (allowEditCompleteOnBlur) {
119
146
  onEditComplete?.(value ?? '')
120
147
  clearTimer()
121
148
  }
122
149
  }}
150
+ onPressIn={(event) => {
151
+ if (interactive) {
152
+ setIsPressed(true)
153
+ }
154
+ props.onPressIn?.(event)
155
+ }}
156
+ onPressOut={(event) => {
157
+ setIsPressed(false)
158
+ props.onPressOut?.(event)
159
+ }}
160
+ onPointerEnter={(event) => {
161
+ if (interactive) {
162
+ setIsHovered(true)
163
+ }
164
+ props.onPointerEnter?.(event)
165
+ }}
166
+ onPointerLeave={(event) => {
167
+ setIsHovered(false)
168
+ props.onPointerLeave?.(event)
169
+ }}
123
170
  onSubmitEditing={(event) => {
124
171
  props.onSubmitEditing?.(event)
125
172
  if (allowEnterComplete) {
@@ -127,8 +174,8 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
127
174
  clearTimer()
128
175
  }
129
176
  }}
130
- placeholderTextColor={placeholderColor}
131
- style={[resolvedInputStyle, style]}
177
+ placeholderTextColor={resolvedPlaceholderStyle.color}
178
+ style={[resolvedContainerStyle, resolvedTextStyle, style]}
132
179
  accessibilityState={{ disabled, selected: required }}
133
180
  />
134
181
  )
@@ -3,16 +3,18 @@ import {
3
3
  FlatList,
4
4
  Modal,
5
5
  Pressable,
6
- Text,
7
- TextInput,
8
6
  View,
9
7
  type StyleProp,
10
8
  type ViewStyle
11
9
  } from 'react-native'
12
- import { Check } from 'lucide-react-native'
13
-
10
+ import type { ColorPairToken } from '@helpwave/hightide-design/theme-tokens'
14
11
  import { Chip } from '../visualization-and-display/Chip'
15
- import { Icon } from '../visualization-and-display/Icon'
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'
16
18
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
17
19
  import {
18
20
  useMultiSelect,
@@ -23,6 +25,7 @@ import type {
23
25
  FormFieldDataHandling,
24
26
  FormFieldInteractionStates
25
27
  } from '../../types/formField'
28
+ import { useTranslation } from '@helpwave/hightide-utils/context'
26
29
 
27
30
  export type MultiSelectOption = UseMultiSelectOption
28
31
 
@@ -34,6 +37,7 @@ export type MultiSelectProps = Partial<FormFieldDataHandling<string[]>>
34
37
  initialValue?: string[],
35
38
  placeholder?: string,
36
39
  showSearch?: boolean,
40
+ color?: ColorPairToken,
37
41
  style?: StyleProp<ViewStyle>,
38
42
  }
39
43
 
@@ -43,6 +47,7 @@ export const MultiSelect = ({
43
47
  initialValue = [],
44
48
  placeholder = 'Select…',
45
49
  showSearch = true,
50
+ color,
46
51
  disabled = false,
47
52
  readOnly = false,
48
53
  invalid = false,
@@ -51,6 +56,7 @@ export const MultiSelect = ({
51
56
  style,
52
57
  }: MultiSelectProps) => {
53
58
  const { theme } = useTheme()
59
+ const translation = useTranslation()
54
60
  const interactive = !disabled && !readOnly
55
61
 
56
62
  const multiSelect = useMultiSelect({
@@ -61,66 +67,105 @@ export const MultiSelect = ({
61
67
  onEditComplete,
62
68
  })
63
69
 
64
- const selectedLabels = useMemo(() => {
65
- return options
66
- .filter((option) => multiSelect.isSelected(option.id))
67
- .map((option) => option.label ?? option.id)
70
+ const selectedOptions = useMemo(() => {
71
+ return options.filter((option) => multiSelect.isSelected(option.id))
68
72
  }, [multiSelect, options])
69
73
 
70
74
  const state = useMemo((): MultiSelectState => ({
75
+ color,
71
76
  isDisabled: disabled,
72
- isReadOnly: readOnly,
77
+ isReadonly: readOnly,
73
78
  isInvalid: invalid,
74
79
  isOpen: multiSelect.isOpen,
75
- hasSelections: selectedLabels.length > 0,
76
- hasValue: selectedLabels.length > 0,
77
- }), [disabled, invalid, multiSelect.isOpen, readOnly, selectedLabels.length])
80
+ hasSelections: selectedOptions.length > 0,
81
+ hasValue: selectedOptions.length > 0,
82
+ }), [color, disabled, invalid, multiSelect.isOpen, readOnly, selectedOptions.length])
78
83
 
79
84
  const multiSelectTheme = theme.components.multiSelect
80
85
 
81
- const resolvedTriggerStyle = useMemo(
82
- () => multiSelectTheme.trigger(state),
83
- [multiSelectTheme, state]
84
- )
85
- const resolvedTriggerTextStyle = useMemo(
86
- () => multiSelectTheme.triggerText(state),
87
- [multiSelectTheme, state]
88
- )
89
86
  const resolvedOverlayStyle = useMemo(
90
- () => multiSelectTheme.overlay(state),
91
- [multiSelectTheme, state]
87
+ () => multiSelectTheme.overlay({}),
88
+ [multiSelectTheme]
92
89
  )
93
90
  const resolvedMenuStyle = useMemo(
94
- () => multiSelectTheme.menu(state),
95
- [multiSelectTheme, state]
91
+ () => multiSelectTheme.menu({ hasSearch: showSearch }),
92
+ [multiSelectTheme, showSearch]
96
93
  )
97
- const resolvedSearchStyle = useMemo(
98
- () => multiSelectTheme.search(state),
99
- [multiSelectTheme, state]
94
+ const resolvedHeaderStyle = useMemo(
95
+ () => multiSelectTheme.header({}),
96
+ [multiSelectTheme]
100
97
  )
101
- const searchPlaceholderColor = useMemo(
102
- () => multiSelectTheme.searchPlaceholderColor(state),
103
- [multiSelectTheme, state]
98
+ const resolvedEmptyTextStyle = useMemo(
99
+ () => multiSelectTheme.emptyText({}),
100
+ [multiSelectTheme]
104
101
  )
102
+ const visibleOptions = useMemo(
103
+ () => options.filter((option) => multiSelect.visibleOptionIds.includes(option.id)),
104
+ [options, multiSelect.visibleOptionIds]
105
+ )
106
+ const showEmptySearchResults = showSearch
107
+ && multiSelect.searchQuery.trim().length > 0
108
+ && visibleOptions.length === 0
105
109
 
106
110
  return (
107
- <View style={style}>
111
+ <View style={[{ width: '100%' }, style]}>
108
112
  <Pressable
109
113
  disabled={!interactive}
110
114
  onPress={() => multiSelect.toggleOpen()}
111
- style={resolvedTriggerStyle}
115
+ style={(pressableState) => {
116
+ const interaction = pressableState as {
117
+ pressed: boolean,
118
+ hovered?: boolean,
119
+ focused?: boolean,
120
+ focusVisible?: boolean,
121
+ }
122
+ return multiSelectTheme.trigger({
123
+ ...state,
124
+ isPressed: interaction.pressed,
125
+ isHovered: !!interaction.hovered,
126
+ isFocused: !!interaction.focused,
127
+ isFocusVisible: !!interaction.focusVisible,
128
+ })
129
+ }}
112
130
  >
113
- {selectedLabels.length > 0
131
+ {selectedOptions.length > 0
114
132
  ? (
115
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
116
- {selectedLabels.map((label) => (
117
- <Chip key={label} size="sm" color="primary" coloringStyle="tonal">
118
- {label}
133
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6, flex: 1 }}>
134
+ {selectedOptions.map((option) => (
135
+ <Chip key={option.id} size="md" color={color} variant="tonal">
136
+ <ThemedText>{option.label ?? option.id}</ThemedText>
137
+ {!state.isReadonly && (
138
+ <View
139
+ style={{
140
+ position: 'relative',
141
+ width: theme.icongraphy.sizes.sm,
142
+ }}
143
+ >
144
+ <IconButton
145
+ accessibilityLabel={translation('remove')}
146
+ size="sm"
147
+ color={theme.colors.negative}
148
+ variant="foreground"
149
+ disabled={!interactive}
150
+ onPress={() => multiSelect.toggleSelection(option.id, false)}
151
+ style={{
152
+ position: 'absolute',
153
+ left: '50%',
154
+ top: '50%',
155
+ transform: [
156
+ { translateX: '-50%' },
157
+ { translateY: '-50%' },
158
+ ],
159
+ }}
160
+ icon={HightideIconRegistry.X}
161
+ />
162
+ </View>
163
+ )}
119
164
  </Chip>
120
165
  ))}
121
166
  </View>
122
167
  )
123
- : <Text style={resolvedTriggerTextStyle}>{placeholder}</Text>}
168
+ : <ThemedText style={multiSelectTheme.triggerText(state)}>{placeholder}</ThemedText>}
124
169
  </Pressable>
125
170
 
126
171
  <Modal
@@ -138,45 +183,67 @@ export const MultiSelect = ({
138
183
  onPress={(event) => event.stopPropagation()}
139
184
  >
140
185
  {showSearch && (
141
- <TextInput
142
- value={multiSelect.searchQuery}
143
- onChangeText={multiSelect.setSearchQuery}
144
- placeholder="Search…"
145
- placeholderTextColor={searchPlaceholderColor}
146
- style={resolvedSearchStyle}
147
- />
186
+ <View style={resolvedHeaderStyle}>
187
+ <SearchBar
188
+ value={multiSelect.searchQuery}
189
+ onValueChange={multiSelect.setSearchQuery}
190
+ onSearch={multiSelect.setSearchQuery}
191
+ />
192
+ </View>
148
193
  )}
149
- <FlatList
150
- data={options.filter((option) => multiSelect.visibleOptionIds.includes(option.id))}
151
- keyExtractor={(option) => option.id}
152
- renderItem={({ item }) => {
153
- const selected = multiSelect.isSelected(item.id)
154
- const isHighlighted = multiSelect.highlightedId === item.id
155
- const optionState = {
156
- isSelected: selected,
157
- isHighlighted,
158
- isDisabled: item.disabled,
159
- }
160
- const checkboxIcon = multiSelectTheme.checkboxIcon(optionState)
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 selected = multiSelect.isSelected(item.id)
213
+ const isHighlighted = multiSelect.highlightedId === item.id
214
+ const optionState = {
215
+ color,
216
+ isSelected: selected,
217
+ isHighlighted,
218
+ isDisabled: item.disabled,
219
+ }
220
+ const checkboxIcon = multiSelectTheme.checkboxIcon(optionState)
221
+ const optionColor = selected
222
+ ? (color ?? theme.colors.primary)
223
+ : undefined
161
224
 
162
- return (
163
- <Pressable
164
- disabled={item.disabled}
165
- onPress={() => multiSelect.toggleSelection(item.id)}
166
- style={multiSelectTheme.option(optionState)}
167
- >
168
- <View style={multiSelectTheme.checkbox(optionState)}>
169
- {checkboxIcon.visible && (
170
- <Icon icon={Check} size="sm" color={checkboxIcon.color} />
225
+ return (
226
+ <ListActionItem
227
+ label={item.label ?? item.id}
228
+ color={optionColor}
229
+ disabled={item.disabled}
230
+ onPress={() => multiSelect.toggleSelection(item.id)}
231
+ leading={(
232
+ <View style={multiSelectTheme.checkbox(optionState)}>
233
+ {checkboxIcon.visible && (
234
+ <ThemedIcon
235
+ icon={HightideIconRegistry.Check}
236
+ size="sm"
237
+ color={checkboxIcon.color}
238
+ />
239
+ )}
240
+ </View>
171
241
  )}
172
- </View>
173
- <Text style={multiSelectTheme.optionText(optionState)}>
174
- {item.label}
175
- </Text>
176
- </Pressable>
177
- )
178
- }}
179
- />
242
+ />
243
+ )
244
+ }}
245
+ />
246
+ )}
180
247
  </Pressable>
181
248
  </Pressable>
182
249
  </Modal>