@helpwave/hightide-native 0.4.0 → 0.6.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.
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "access": "public"
11
11
  },
12
12
  "license": "AGPL-3.0-only",
13
- "version": "0.4.0",
13
+ "version": "0.6.0",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "src"
@@ -39,12 +39,16 @@
39
39
  "./types": {
40
40
  "types": "./src/types/index.ts",
41
41
  "default": "./src/types/index.ts"
42
+ },
43
+ "./utils": {
44
+ "types": "./src/utils/index.ts",
45
+ "default": "./src/utils/index.ts"
42
46
  }
43
47
  },
44
48
  "dependencies": {
45
49
  "lucide-react-native": "0.561.0",
46
- "@helpwave/hightide-utils": "0.1.0",
47
- "@helpwave/hightide-design": "0.4.0"
50
+ "@helpwave/hightide-design": "0.6.0",
51
+ "@helpwave/hightide-utils": "0.1.0"
48
52
  },
49
53
  "peerDependencies": {
50
54
  "@react-native-async-storage/async-storage": ">=2.0.0",
@@ -39,6 +39,7 @@ export type ChatAttachmentMessageBubbleProps = Omit<ChatMessageBubbleProps, 'chi
39
39
  icon?: ReactNode,
40
40
  downloadLabel?: string,
41
41
  onDownload?: () => void,
42
+ style: ChatMessageBubbleProps['style'],
42
43
  contentContainerStyle?: StyleOverwrite<
43
44
  PressableState,
44
45
  PressableContainerStyle
@@ -89,7 +90,6 @@ export const ChatAttachmentMessageBubble = ({
89
90
  timestamp,
90
91
  readReceipt,
91
92
  style,
92
- containerStyle,
93
93
  bodyStyle,
94
94
  bodyTextStyle,
95
95
  metaDataContainerStyle,
@@ -137,9 +137,9 @@ export const ChatAttachmentMessageBubble = ({
137
137
  const resolvedContainerStyle = useMemo(
138
138
  () => mergeBubbleStyleOverwrite<ChatMessageBubbleContainerStyle>(
139
139
  bubbleOverrides.container(state),
140
- containerStyle
140
+ style
141
141
  ),
142
- [bubbleOverrides, state, containerStyle]
142
+ [bubbleOverrides, state, style]
143
143
  )
144
144
  const resolvedBodyStyle = useMemo(
145
145
  () => mergeBubbleStyleOverwrite<ChatMessageBubbleBodyStyle>(
@@ -183,8 +183,7 @@ export const ChatAttachmentMessageBubble = ({
183
183
  direction={direction}
184
184
  timestamp={timestamp}
185
185
  readReceipt={readReceipt}
186
- style={style}
187
- containerStyle={resolvedContainerStyle}
186
+ style={resolvedContainerStyle}
188
187
  bodyStyle={resolvedBodyStyle}
189
188
  bodyTextStyle={resolvedBodyTextStyle}
190
189
  metaDataContainerStyle={resolvedMetaDataContainerStyle}
@@ -219,7 +218,7 @@ export const ChatAttachmentMessageBubble = ({
219
218
  />
220
219
  )}
221
220
  </View>
222
- <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
221
+ <View style={{ gap: theme.spacing.xs }}>
223
222
  {typeof name === 'string' || typeof name === 'number' ? (
224
223
  <ThemedText style={resolvedFileNameTextStyle} numberOfLines={1}>{name}</ThemedText>
225
224
  ) : (
@@ -4,9 +4,7 @@ import {
4
4
  } from 'react'
5
5
  import {
6
6
  View,
7
- type StyleProp,
8
- type ViewProps,
9
- type ViewStyle
7
+ type ViewProps
10
8
  } from 'react-native'
11
9
  import { HightideIconRegistry } from '../../icons/HightideIconRegistry'
12
10
  import { ThemedIcon } from '../visualization-and-display/ThemedIcon'
@@ -31,8 +29,7 @@ export type ChatMessageBubbleProps = Omit<ViewProps, 'children' | 'style'> & {
31
29
  timestamp: ReactNode,
32
30
  readReceipt?: ReactNode,
33
31
  children?: ReactNode,
34
- style?: StyleProp<ViewStyle>,
35
- containerStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
32
+ style?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleContainerStyle>,
36
33
  bodyStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleBodyStyle>,
37
34
  bodyTextStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleBodyTextStyle>,
38
35
  metaDataContainerStyle?: StyleOverwrite<ChatMessageBubbleState, ChatMessageBubbleMetaDataContainerStyle>,
@@ -46,7 +43,6 @@ export const ChatMessageBubble = ({
46
43
  readReceipt,
47
44
  children,
48
45
  style,
49
- containerStyle,
50
46
  bodyStyle,
51
47
  bodyTextStyle,
52
48
  metaDataContainerStyle,
@@ -58,8 +54,8 @@ export const ChatMessageBubble = ({
58
54
  const state = useMemo(() => ({ direction }), [direction])
59
55
 
60
56
  const resolvedContainerStyle = useMemo(
61
- () => theme.components.chat.messageBubble.container(state, containerStyle),
62
- [theme, state, containerStyle]
57
+ () => theme.components.chat.messageBubble.container(state, style),
58
+ [theme, state, style]
63
59
  )
64
60
  const resolvedBodyStyle = useMemo(
65
61
  () => theme.components.chat.messageBubble.body(state, bodyStyle),
@@ -87,7 +83,7 @@ export const ChatMessageBubble = ({
87
83
  )
88
84
 
89
85
  return (
90
- <View {...props} style={[resolvedContainerStyle, style]}>
86
+ <View {...props} style={resolvedContainerStyle}>
91
87
  <View style={resolvedBodyStyle}>
92
88
  {typeof children === 'string' || typeof children === 'number' ? (
93
89
  <ThemedText style={resolvedBodyTextStyle}>{children}</ThemedText>
@@ -3,11 +3,12 @@ import {
3
3
  useMemo,
4
4
  useState
5
5
  } from 'react'
6
+ import type {
7
+ TextStyle } from 'react-native'
6
8
  import {
9
+ StyleSheet,
7
10
  TextInput,
8
- type StyleProp,
9
- type TextInputProps,
10
- type TextStyle
11
+ type TextInputProps
11
12
  } from 'react-native'
12
13
 
13
14
  import {
@@ -51,8 +52,7 @@ export type InputProps = Omit<TextInputProps, 'value' | 'style'>
51
52
  color?: ColorPairToken,
52
53
  editCompleteOptions?: EditCompleteOptions,
53
54
  initialValue?: string,
54
- style?: StyleProp<TextStyle>,
55
- containerStyle?: StyleOverwrite<InputState, InputContainerStyle>,
55
+ style?: StyleOverwrite<InputState, InputContainerStyle>,
56
56
  textStyle?: StyleOverwrite<InputState, InputTextStyle>,
57
57
  }
58
58
 
@@ -68,7 +68,6 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
68
68
  onEditComplete,
69
69
  editCompleteOptions,
70
70
  style,
71
- containerStyle,
72
71
  textStyle,
73
72
  ...props
74
73
  }, ref) {
@@ -108,8 +107,8 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
108
107
  }), [color, disabled, invalid, readOnly, interactive, isHovered, isPressed, isFocused])
109
108
 
110
109
  const resolvedContainerStyle = useMemo(
111
- () => theme.components.input.container(state, containerStyle),
112
- [theme, state, containerStyle]
110
+ () => theme.components.input.container(state, style),
111
+ [theme, state, style]
113
112
  )
114
113
  const resolvedTextStyle = useMemo(
115
114
  () => theme.components.input.text(state, textStyle),
@@ -120,6 +119,21 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
120
119
  [theme, state]
121
120
  )
122
121
 
122
+
123
+ const resolvedStyle = useMemo(
124
+ () => {
125
+ const resolvedSheet: TextStyle = StyleSheet.flatten<TextStyle>([
126
+ resolvedContainerStyle,
127
+ resolvedTextStyle,
128
+ ])
129
+ // this is required for android to work
130
+ // TODO find a better solution like separating container and text input
131
+ delete resolvedSheet['boxShadow']
132
+ return resolvedSheet
133
+ },
134
+ [resolvedContainerStyle, resolvedTextStyle]
135
+ )
136
+
123
137
  return (
124
138
  <TextInput
125
139
  {...props}
@@ -175,7 +189,7 @@ export const Input = forwardRef<TextInput, InputProps>(function Input({
175
189
  }
176
190
  }}
177
191
  placeholderTextColor={resolvedPlaceholderStyle.color}
178
- style={[resolvedContainerStyle, resolvedTextStyle, style]}
192
+ style={resolvedStyle}
179
193
  accessibilityState={{ disabled, selected: required }}
180
194
  />
181
195
  )
@@ -36,7 +36,7 @@ export type MultiSelectProps = Partial<FormFieldDataHandling<string[]>>
36
36
  value?: string[],
37
37
  initialValue?: string[],
38
38
  placeholder?: string,
39
- showSearch?: boolean,
39
+ searchableThreshold?: number,
40
40
  color?: ColorPairToken,
41
41
  style?: StyleProp<ViewStyle>,
42
42
  }
@@ -46,7 +46,7 @@ export const MultiSelect = ({
46
46
  value: controlledValue,
47
47
  initialValue = [],
48
48
  placeholder = 'Select…',
49
- showSearch = true,
49
+ searchableThreshold = 6,
50
50
  color,
51
51
  disabled = false,
52
52
  readOnly = false,
@@ -67,6 +67,8 @@ export const MultiSelect = ({
67
67
  onEditComplete,
68
68
  })
69
69
 
70
+ const isSearchVisible = options.length >= searchableThreshold
71
+
70
72
  const selectedOptions = useMemo(() => {
71
73
  return options.filter((option) => multiSelect.isSelected(option.id))
72
74
  }, [multiSelect, options])
@@ -88,8 +90,8 @@ export const MultiSelect = ({
88
90
  [multiSelectTheme]
89
91
  )
90
92
  const resolvedMenuStyle = useMemo(
91
- () => multiSelectTheme.menu({ hasSearch: showSearch }),
92
- [multiSelectTheme, showSearch]
93
+ () => multiSelectTheme.menu({ hasSearch: isSearchVisible }),
94
+ [multiSelectTheme, isSearchVisible]
93
95
  )
94
96
  const resolvedHeaderStyle = useMemo(
95
97
  () => multiSelectTheme.header({}),
@@ -103,7 +105,7 @@ export const MultiSelect = ({
103
105
  () => options.filter((option) => multiSelect.visibleOptionIds.includes(option.id)),
104
106
  [options, multiSelect.visibleOptionIds]
105
107
  )
106
- const showEmptySearchResults = showSearch
108
+ const showEmptySearchResults = isSearchVisible
107
109
  && multiSelect.searchQuery.trim().length > 0
108
110
  && visibleOptions.length === 0
109
111
 
@@ -160,6 +162,7 @@ export const MultiSelect = ({
160
162
  style={{
161
163
  position: 'relative',
162
164
  width: theme.icongraphy.sizes.sm,
165
+ height: theme.icongraphy.sizes.sm,
163
166
  }}
164
167
  >
165
168
  <IconButton
@@ -210,7 +213,7 @@ export const MultiSelect = ({
210
213
  style={resolvedMenuStyle}
211
214
  onPress={(event) => event.stopPropagation()}
212
215
  >
213
- {showSearch && (
216
+ {isSearchVisible && (
214
217
  <View style={resolvedHeaderStyle}>
215
218
  <SearchBar
216
219
  value={multiSelect.searchQuery}
@@ -260,7 +263,8 @@ export const MultiSelect = ({
260
263
  <View style={multiSelectTheme.checkbox(optionState)}>
261
264
  <ThemedIcon
262
265
  icon={HightideIconRegistry.Check}
263
- size="sm"
266
+ size={checkboxIcon.size}
267
+ strokeWidth={checkboxIcon.strokeWidth}
264
268
  color={checkboxIcon.color}
265
269
  />
266
270
  </View>
@@ -20,7 +20,6 @@ import {
20
20
  type UseSelectOption
21
21
  } from '../../hooks/useSelect'
22
22
  import type { SelectState } from '../../theme/types/components/select'
23
- import type { Color } from '../../theme/types/color'
24
23
  import type {
25
24
  FormFieldDataHandling,
26
25
  FormFieldInteractionStates
@@ -38,7 +37,7 @@ export type SelectProps = Partial<FormFieldDataHandling<string>>
38
37
  value?: string | null,
39
38
  initialValue?: string | null,
40
39
  placeholder?: string,
41
- showSearch?: boolean,
40
+ searchableThreshold?: number,
42
41
  color?: ColorPairToken,
43
42
  style?: StyleProp<ViewStyle>,
44
43
  }
@@ -48,7 +47,7 @@ export const Select = ({
48
47
  value: controlledValue,
49
48
  initialValue = null,
50
49
  placeholder = 'Select…',
51
- showSearch = true,
50
+ searchableThreshold = 6,
52
51
  color,
53
52
  disabled = false,
54
53
  readOnly = false,
@@ -69,6 +68,8 @@ export const Select = ({
69
68
  onEditComplete,
70
69
  })
71
70
 
71
+ const isSearchVisible = options.length >= searchableThreshold
72
+
72
73
  const selectedLabel = useMemo(() => {
73
74
  const selected = options.find((option) => option.id === select.value)
74
75
  return selected?.label ?? placeholder
@@ -95,8 +96,8 @@ export const Select = ({
95
96
  [selectTheme]
96
97
  )
97
98
  const resolvedMenuStyle = useMemo(
98
- () => selectTheme.menu({ hasSearch: showSearch }),
99
- [selectTheme, showSearch]
99
+ () => selectTheme.menu({ hasSearch: isSearchVisible }),
100
+ [selectTheme, isSearchVisible]
100
101
  )
101
102
  const resolvedHeaderStyle = useMemo(
102
103
  () => selectTheme.header({}),
@@ -106,7 +107,7 @@ export const Select = ({
106
107
  () => selectTheme.emptyText({}),
107
108
  [selectTheme]
108
109
  )
109
- const showEmptySearchResults = showSearch
110
+ const showEmptySearchResults = isSearchVisible
110
111
  && select.searchQuery.trim().length > 0
111
112
  && visibleOptions.length === 0
112
113
 
@@ -181,7 +182,7 @@ export const Select = ({
181
182
  style={resolvedMenuStyle}
182
183
  onPress={(event) => event.stopPropagation()}
183
184
  >
184
- {showSearch && (
185
+ {isSearchVisible && (
185
186
  <View style={resolvedHeaderStyle}>
186
187
  <SearchBar
187
188
  value={select.searchQuery}
@@ -220,16 +221,14 @@ export const Select = ({
220
221
  color={optionColor}
221
222
  disabled={item.disabled}
222
223
  onPress={() => select.selectValue(item.id)}
223
- trailing={isSelected
224
- ? (
225
- <ThemedIcon
226
- icon={HightideIconRegistry.Check}
227
- size={checkIcon.size}
228
- strokeWidth={checkIcon.strokeWidth}
229
- color={checkIcon.color as Color | undefined}
230
- />
231
- )
232
- : undefined}
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
+ )}
233
232
  />
234
233
  )
235
234
  }}
@@ -0,0 +1,26 @@
1
+ import { forwardRef } from 'react'
2
+ import type { TextInput } from 'react-native'
3
+
4
+ import { useTheme } from '../../global-contexts/theme/ThemeContext'
5
+ import { Input, type InputProps } from './Input'
6
+
7
+ export type TextareaProps = InputProps
8
+
9
+ export const Textarea = forwardRef<TextInput, TextareaProps>(function Textarea({
10
+ style,
11
+ textStyle,
12
+ ...props
13
+ }, ref) {
14
+ const { theme } = useTheme()
15
+
16
+ return (
17
+ <Input
18
+ {...props}
19
+ ref={ref}
20
+ multiline
21
+ textAlignVertical="top"
22
+ style={(state) => theme.components.textarea.container(state, style)}
23
+ textStyle={(state) => theme.components.textarea.text(state, textStyle)}
24
+ />
25
+ )
26
+ })
@@ -6,4 +6,5 @@ export * from './MultiSelect'
6
6
  export * from './SearchBar'
7
7
  export * from './Select'
8
8
  export * from './Switch'
9
+ export * from './Textarea'
9
10
  export * from './ThemedPressable'
@@ -5,13 +5,13 @@ import {
5
5
  type ViewProps
6
6
  } from 'react-native'
7
7
 
8
- import type { HexColorToken } from '@helpwave/hightide-design/primitive-tokens'
9
8
  import type { Appearance } from '@helpwave/hightide-design/semantic-token-resolvers'
10
9
  import type { IconSize } from '@helpwave/hightide-design/theme-tokens'
11
10
 
12
11
  import type { IconComponent } from '../../icons/types'
13
12
  import { useContentTheme } from '../../global-contexts/content-theme/ContentThemeContext'
14
13
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
14
+ import { HexColorUtils } from '../../utils/hex'
15
15
 
16
16
  export type ThemedIconAppearance = Appearance
17
17
 
@@ -33,7 +33,7 @@ export const ThemedIcon = ({
33
33
  ...props
34
34
  }: ThemedIconProps) => {
35
35
  const { theme } = useTheme()
36
- const { foreground, iconStyle } = useContentTheme()
36
+ const { foreground, background, iconStyle } = useContentTheme()
37
37
  const resolvedSize = size ?? iconStyle.size ?? 'md'
38
38
  const iconToken = typeof resolvedSize === 'number'
39
39
  ? {
@@ -42,13 +42,16 @@ export const ThemedIcon = ({
42
42
  }
43
43
  : theme.components.icon[resolvedSize]
44
44
 
45
- // TODO fix this by using a hex color parser
46
- const baseColor = (color
47
- ?? iconStyle.color
48
- ?? foreground) as HexColorToken
45
+ const baseColor = HexColorUtils.tryParseColorValue(color ?? iconStyle.color ?? foreground) ?? foreground
49
46
  const resolvedColor = appearance === 'normal'
50
47
  ? baseColor
51
- : theme.semantics.withAppearance({ color: baseColor, appearance })
48
+ : theme.semantics.withAppearance({
49
+ colorPair: {
50
+ color: background,
51
+ onColor: baseColor,
52
+ },
53
+ appearance,
54
+ })
52
55
 
53
56
  return (
54
57
  <View
@@ -6,8 +6,6 @@ import {
6
6
  type TextProps
7
7
  } from 'react-native'
8
8
 
9
- import type { HexColorToken } from '@helpwave/hightide-design/primitive-tokens'
10
-
11
9
  import { useContentTheme } from '../../global-contexts/content-theme/ContentThemeContext'
12
10
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
13
11
 
@@ -23,20 +21,21 @@ export const ThemedText = forwardRef<React.ComponentRef<typeof RNText>, ThemedTe
23
21
  ...props
24
22
  }, ref) {
25
23
  const { theme } = useTheme()
26
- const { foreground, textStyle } = useContentTheme()
27
- // TODO use a hex color parser
28
- const foregroundColor = (typeof textStyle.color === 'string'
29
- ? textStyle.color
30
- : foreground) as HexColorToken
24
+ const { foreground, background, textStyle } = useContentTheme()
31
25
  const color = appearance === 'description'
32
- ? theme.semantics.asDescription({ color: foregroundColor })
33
- : foregroundColor
26
+ ? theme.semantics.asDescription({
27
+ colorPair: {
28
+ color: background,
29
+ onColor: foreground,
30
+ },
31
+ })
32
+ : undefined
34
33
 
35
34
  return (
36
35
  <RNText
37
36
  {...props}
38
37
  ref={ref}
39
- style={[textStyle, { color }, style]}
38
+ style={[textStyle, color === undefined ? undefined : { color }, style]}
40
39
  />
41
40
  )
42
41
  })
@@ -2,16 +2,18 @@ import {
2
2
  createContext,
3
3
  useContext
4
4
  } from 'react'
5
- import type { ColorValue, TextStyle } from 'react-native'
5
+ import type { TextStyle } from 'react-native'
6
+
7
+ import type { HexColorToken } from '@helpwave/hightide-design/primitive-tokens'
6
8
 
7
9
  import type { IconStyle } from '../../icons'
8
10
 
9
- export type ContentThemeContextValue = {
10
- foreground: ColorValue,
11
- background: ColorValue,
12
- textStyle: TextStyle,
13
- iconStyle: IconStyle,
14
- }
11
+ export type ContentThemeContextValue = Readonly<{
12
+ foreground: HexColorToken,
13
+ background: HexColorToken,
14
+ textStyle: Readonly<TextStyle>,
15
+ iconStyle: Readonly<IconStyle>,
16
+ }>
15
17
 
16
18
  export const ContentThemeContext = createContext<ContentThemeContextValue | null>(null)
17
19
 
@@ -4,7 +4,10 @@ import {
4
4
  } from 'react'
5
5
  import type { ColorValue, TextStyle } from 'react-native'
6
6
 
7
+ import type { HexColorToken } from '@helpwave/hightide-design/primitive-tokens'
8
+
7
9
  import type { IconStyle } from '../../icons'
10
+ import { HexColorUtils } from '../../utils/hex'
8
11
  import {
9
12
  ContentThemeContext,
10
13
  useContentTheme,
@@ -36,15 +39,15 @@ export const ContentThemeRootProvider = ({
36
39
 
37
40
  type ColorOverride =
38
41
  | ColorValue
39
- | ((prev: ColorValue) => ColorValue)
42
+ | ((prev: HexColorToken) => ColorValue)
40
43
 
41
44
  type TextStyleOverride =
42
45
  | TextStyle
43
- | ((prev: TextStyle, foreground: ColorValue) => TextStyle)
46
+ | ((prev: TextStyle, foreground: HexColorToken) => TextStyle)
44
47
 
45
48
  type IconStyleOverride =
46
49
  | IconStyle
47
- | ((prev: IconStyle, foreground: ColorValue) => IconStyle)
50
+ | ((prev: IconStyle, foreground: HexColorToken) => IconStyle)
48
51
 
49
52
  export type ContentThemeOverrideProviderProps = PropsWithChildren & {
50
53
  foreground?: ColorOverride,
@@ -55,10 +58,22 @@ export type ContentThemeOverrideProviderProps = PropsWithChildren & {
55
58
  isKeepingIconColor?: boolean,
56
59
  }
57
60
 
61
+ const resolveColorOverride = (
62
+ override: ColorOverride | undefined,
63
+ previous: HexColorToken
64
+ ): HexColorToken | undefined => {
65
+ if (override === undefined) {
66
+ return undefined
67
+ }
68
+
69
+ const raw = typeof override === 'function' ? override(previous) : override
70
+ return HexColorUtils.tryParseColorValue(raw)
71
+ }
72
+
58
73
  export const ContentThemeOverrideProvider = ({
59
74
  children,
60
75
  foreground: foregroundOverride,
61
- background: backroundOverride,
76
+ background: backgroundOverride,
62
77
  textStyle: textStyleOverride,
63
78
  iconStyle: iconStyleOverride,
64
79
  isKeepingIconColor = false,
@@ -67,25 +82,24 @@ export const ContentThemeOverrideProvider = ({
67
82
  const parent = useContentTheme()
68
83
 
69
84
  const value = useMemo((): ContentThemeContextValue => {
70
- const newForeground = typeof foregroundOverride === 'function'
71
- ? foregroundOverride(parent.foreground)
72
- : foregroundOverride
85
+ const newForeground = resolveColorOverride(foregroundOverride, parent.foreground)
86
+ const newBackground = resolveColorOverride(backgroundOverride, parent.background)
73
87
 
74
- const newBackground = typeof backroundOverride === 'function'
75
- ? backroundOverride(parent.background)
76
- : backroundOverride
77
-
78
- const textStyle = typeof textStyleOverride === 'function'
88
+ let textStyle = typeof textStyleOverride === 'function'
79
89
  ? textStyleOverride(parent.textStyle, parent.foreground)
80
90
  : textStyleOverride ?? parent.textStyle
81
91
 
82
- if(!isKeepingTextColor && newForeground !== undefined) textStyle['color'] = newForeground
92
+ if (!isKeepingTextColor && newForeground !== undefined) {
93
+ textStyle = { ...textStyle, color: newForeground }
94
+ }
83
95
 
84
- const iconStyle = typeof iconStyleOverride === 'function'
96
+ let iconStyle = typeof iconStyleOverride === 'function'
85
97
  ? iconStyleOverride(parent.iconStyle, parent.foreground)
86
98
  : iconStyleOverride ?? parent.iconStyle
87
99
 
88
- if(!isKeepingIconColor && newForeground !== undefined) iconStyle['color'] = newForeground
100
+ if (!isKeepingIconColor && newForeground !== undefined) {
101
+ iconStyle = { ...iconStyle, color: newForeground }
102
+ }
89
103
 
90
104
  return {
91
105
  foreground: newForeground ?? parent.foreground,
@@ -93,7 +107,7 @@ export const ContentThemeOverrideProvider = ({
93
107
  textStyle,
94
108
  iconStyle,
95
109
  }
96
- }, [parent, foregroundOverride, textStyleOverride, iconStyleOverride, backroundOverride, isKeepingTextColor, isKeepingIconColor])
110
+ }, [parent, foregroundOverride, textStyleOverride, iconStyleOverride, backgroundOverride, isKeepingTextColor, isKeepingIconColor])
97
111
 
98
112
  return (
99
113
  <ContentThemeContext.Provider value={value}>
@@ -95,6 +95,7 @@ export const ThemeProvider = ({
95
95
  <ThemeContext.Provider value={contextValue}>
96
96
  <ContentThemeRootProvider
97
97
  foreground={contextValue.theme.colors.surface.onColor}
98
+ background={contextValue.theme.colors.surface.color}
98
99
  textStyle={contextValue.theme.typography.body.md}
99
100
  iconStyle={{
100
101
  size: contextValue.theme.icongraphy.sizes.md,
@@ -14,4 +14,5 @@ export * from './multiSelect'
14
14
  export * from './searchBar'
15
15
  export * from './select'
16
16
  export * from './switch'
17
+ export * from './textarea'
17
18
  export * from './themedPressable'
@@ -17,7 +17,7 @@ import {
17
17
  } from '../types/resolver'
18
18
 
19
19
  import { StyleAdapterUtils } from '../adapters'
20
- import { HexColorUtils } from '@helpwave/hightide-design/utils'
20
+ import { HexColorUtils } from '../../utils/hex'
21
21
 
22
22
  const toDesignInputState = (state: InputState = {}): DesignInputState => {
23
23
  const active = new Set<InputStateValue>()
@@ -135,7 +135,6 @@ export const toMultiSelectThemeResolvers: ComponentThemeResolver<MultiSelectThem
135
135
  )),
136
136
  overlay: createSimpleStyleResolver((): SelectOverlayStyle => ({
137
137
  ...StyleAdapterUtils.container(resolve().overlay),
138
- flex: 1,
139
138
  })),
140
139
  menu: createStyleResolver((state: SelectMenuState): SelectMenuStyle => (
141
140
  StyleAdapterUtils.container(resolve({ hasSearch: state.hasSearch }).menu)
@@ -18,7 +18,7 @@ import {
18
18
  } from '../types/resolver'
19
19
 
20
20
  import { StyleAdapterUtils } from '../adapters'
21
- import { HexColorUtils } from '@helpwave/hightide-design/utils'
21
+ import { HexColorUtils } from '../../utils/hex'
22
22
 
23
23
  const toDesignSearchBarState = (state: SearchBarState = {}): DesignInputState => {
24
24
  const active = new Set<InputStateValue>()
@@ -0,0 +1,14 @@
1
+ import type { InputThemeResolvers } from '../types/components/input'
2
+ import type { ComponentThemeResolver } from '../types/resolver'
3
+
4
+ import { toInputThemeResolvers } from './input'
5
+
6
+ export const toTextareaThemeResolvers: ComponentThemeResolver<InputThemeResolvers> = (params) => (
7
+ toInputThemeResolvers({
8
+ ...params,
9
+ componentTokens: {
10
+ ...params.componentTokens,
11
+ input: params.componentTokens.textarea,
12
+ },
13
+ })
14
+ )
@@ -29,6 +29,7 @@ import {
29
29
  import { toMultiSelectThemeResolvers } from '../resolvers/multiSelect'
30
30
  import { toSelectThemeResolvers } from '../resolvers/select'
31
31
  import { toSwitchThemeResolvers } from '../resolvers/switch'
32
+ import { toTextareaThemeResolvers } from '../resolvers/textarea'
32
33
  import { toThemedPressableThemeResolvers } from '../resolvers/themedPressable'
33
34
  import type { HightideThemeSemantics } from '../types/semantics'
34
35
  import type { HightideTheme } from '../types/theme'
@@ -172,6 +173,11 @@ export const createHightideTheme = (themeTokens: ThemeTokens): HightideTheme =>
172
173
  semanticTokens: hightideSemanticTokenResolvers,
173
174
  componentTokens: componentTokenResolvers,
174
175
  }),
176
+ textarea: toTextareaThemeResolvers({
177
+ themeTokens,
178
+ semanticTokens: hightideSemanticTokenResolvers,
179
+ componentTokens: componentTokenResolvers,
180
+ }),
175
181
  searchBar: toSearchBarThemeResolvers({
176
182
  themeTokens,
177
183
  semanticTokens: hightideSemanticTokenResolvers,
@@ -19,6 +19,7 @@ import type { ListItemThemeResolvers } from './listItem'
19
19
  import type { MultiSelectThemeResolvers } from './multiSelect'
20
20
  import type { SelectThemeResolvers } from './select'
21
21
  import type { SwitchThemeResolvers } from './switch'
22
+ import type { TextareaThemeResolvers } from './textarea'
22
23
  import type { ThemedPressableThemeResolvers } from './themedPressable'
23
24
 
24
25
  export type IconThemeResolvers = Record<IconSize, IconTokens>
@@ -31,6 +32,7 @@ export type HightideComponentThemes = {
31
32
  checkbox: CheckboxThemeResolvers,
32
33
  switch: SwitchThemeResolvers,
33
34
  input: InputThemeResolvers,
35
+ textarea: TextareaThemeResolvers,
34
36
  searchBar: SearchBarThemeResolvers,
35
37
  select: SelectThemeResolvers,
36
38
  multiSelect: MultiSelectThemeResolvers,
@@ -13,4 +13,5 @@ export * from './multiSelect'
13
13
  export * from './searchBar'
14
14
  export * from './select'
15
15
  export * from './switch'
16
+ export * from './textarea'
16
17
  export * from './themedPressable'
@@ -0,0 +1,8 @@
1
+ export type {
2
+ InputContainerStyle as TextareaContainerStyle,
3
+ InputIconStyle as TextareaIconStyle,
4
+ InputPlaceholderStyle as TextareaPlaceholderStyle,
5
+ InputState as TextareaState,
6
+ InputTextStyle as TextareaTextStyle,
7
+ InputThemeResolvers as TextareaThemeResolvers
8
+ } from './input'
@@ -61,13 +61,13 @@ export type HightideThemeSemantics = {
61
61
  tintStrength?: TintStrength,
62
62
  }, HexColorToken>,
63
63
  withAppearance: BoundSemanticResolver<{
64
- color: HexColorToken,
64
+ colorPair: ColorPairToken,
65
65
  appearance: Appearance,
66
66
  }, HexColorToken>,
67
67
  asFaded: BoundSemanticResolver<{
68
- color: HexColorToken,
68
+ colorPair: ColorPairToken,
69
69
  }, HexColorToken>,
70
70
  asDescription: BoundSemanticResolver<{
71
- color: HexColorToken,
71
+ colorPair: ColorPairToken,
72
72
  }, HexColorToken>,
73
73
  } & ElementLayoutTokens
@@ -0,0 +1,193 @@
1
+ import { processColor, type ColorValue } from 'react-native'
2
+
3
+ import type { HexColorToken } from '@helpwave/hightide-design/primitive-tokens'
4
+ import { HexColorUtils as DesignHexColorUtils } from '@helpwave/hightide-design/utils'
5
+
6
+ const clampByte = (value: number): number => Math.round(Math.min(255, Math.max(0, value)))
7
+
8
+ const channelHex = (value: number): string => clampByte(value).toString(16).padStart(2, '0')
9
+
10
+ const toHex = (r: number, g: number, b: number, alpha: number = 1): HexColorToken => {
11
+ if (alpha >= 1) {
12
+ return `#${channelHex(r)}${channelHex(g)}${channelHex(b)}`
13
+ }
14
+
15
+ return `#${channelHex(r)}${channelHex(g)}${channelHex(b)}${channelHex(alpha * 255)}`
16
+ }
17
+
18
+ const argbNumberToHex = (argb: number): HexColorToken => {
19
+ const unsigned = argb >>> 0
20
+ const alpha = ((unsigned >>> 24) & 0xff) / 255
21
+ const r = (unsigned >>> 16) & 0xff
22
+ const g = (unsigned >>> 8) & 0xff
23
+ const b = unsigned & 0xff
24
+
25
+ return toHex(r, g, b, alpha)
26
+ }
27
+
28
+ const parseHexString = (value: string): HexColorToken | undefined => {
29
+ const normalized = value.startsWith('#') ? value.slice(1) : value
30
+ if (!/^[0-9a-fA-F]+$/.test(normalized)) {
31
+ return undefined
32
+ }
33
+
34
+ if (normalized.length === 3 || normalized.length === 4) {
35
+ const expanded = normalized.split('').map((char) => `${char}${char}`).join('')
36
+ return parseHexString(expanded)
37
+ }
38
+
39
+ if (normalized.length !== 6 && normalized.length !== 8) {
40
+ return undefined
41
+ }
42
+
43
+ const r = Number.parseInt(normalized.slice(0, 2), 16)
44
+ const g = Number.parseInt(normalized.slice(2, 4), 16)
45
+ const b = Number.parseInt(normalized.slice(4, 6), 16)
46
+ const alpha = normalized.length === 8
47
+ ? Number.parseInt(normalized.slice(6, 8), 16) / 255
48
+ : 1
49
+
50
+ return toHex(r, g, b, alpha)
51
+ }
52
+
53
+ const parseNumericChannel = (raw: string, percentScale: number): number => {
54
+ const trimmed = raw.trim()
55
+ if (trimmed.endsWith('%')) {
56
+ return (Number.parseFloat(trimmed.slice(0, -1)) / 100) * percentScale
57
+ }
58
+
59
+ return Number.parseFloat(trimmed)
60
+ }
61
+
62
+ const parseRgbString = (value: string): HexColorToken | undefined => {
63
+ const match = /^rgba?\(\s*([^\s,/]+)[\s,]+([^\s,/]+)[\s,]+([^\s,/]+)(?:\s*[,/]\s*([^\s,/]+))?\s*\)$/i.exec(value)
64
+ if (!match) {
65
+ return undefined
66
+ }
67
+
68
+ const r = parseNumericChannel(match[1], 255)
69
+ const g = parseNumericChannel(match[2], 255)
70
+ const b = parseNumericChannel(match[3], 255)
71
+ const alpha = match[4] === undefined ? 1 : parseNumericChannel(match[4], 1)
72
+
73
+ if ([r, g, b, alpha].some((channel) => Number.isNaN(channel))) {
74
+ return undefined
75
+ }
76
+
77
+ return toHex(r, g, b, alpha)
78
+ }
79
+
80
+ const hueToRgb = (p: number, q: number, t: number): number => {
81
+ let wrapped = t
82
+ if (wrapped < 0) {
83
+ wrapped += 1
84
+ }
85
+ if (wrapped > 1) {
86
+ wrapped -= 1
87
+ }
88
+ if (wrapped < 1 / 6) {
89
+ return p + (q - p) * 6 * wrapped
90
+ }
91
+ if (wrapped < 1 / 2) {
92
+ return q
93
+ }
94
+ if (wrapped < 2 / 3) {
95
+ return p + (q - p) * (2 / 3 - wrapped) * 6
96
+ }
97
+
98
+ return p
99
+ }
100
+
101
+ const hslToRgb = (hue: number, saturation: number, lightness: number): [number, number, number] => {
102
+ if (saturation === 0) {
103
+ const gray = lightness * 255
104
+ return [gray, gray, gray]
105
+ }
106
+
107
+ const q = lightness < 0.5
108
+ ? lightness * (1 + saturation)
109
+ : lightness + saturation - lightness * saturation
110
+ const p = 2 * lightness - q
111
+ const h = (((hue % 360) + 360) % 360) / 360
112
+
113
+ return [
114
+ hueToRgb(p, q, h + 1 / 3) * 255,
115
+ hueToRgb(p, q, h) * 255,
116
+ hueToRgb(p, q, h - 1 / 3) * 255,
117
+ ]
118
+ }
119
+
120
+ const parseHslString = (value: string): HexColorToken | undefined => {
121
+ const match = /^hsla?\(\s*([^\s,/]+)(?:deg)?[\s,]+([^\s,/]+)[\s,]+([^\s,/]+)(?:\s*[,/]\s*([^\s,/]+))?\s*\)$/i.exec(value)
122
+ if (!match) {
123
+ return undefined
124
+ }
125
+
126
+ const hue = Number.parseFloat(match[1])
127
+ const saturation = parseNumericChannel(match[2], 1)
128
+ const lightness = parseNumericChannel(match[3], 1)
129
+ const alpha = match[4] === undefined ? 1 : parseNumericChannel(match[4], 1)
130
+
131
+ if ([hue, saturation, lightness, alpha].some((channel) => Number.isNaN(channel))) {
132
+ return undefined
133
+ }
134
+
135
+ const [r, g, b] = hslToRgb(hue, saturation, lightness)
136
+ return toHex(r, g, b, alpha)
137
+ }
138
+
139
+ const parseColorLiteral = (color: ColorValue | number): HexColorToken | undefined => {
140
+ if (typeof color === 'number') {
141
+ return argbNumberToHex(color)
142
+ }
143
+
144
+ if (typeof color !== 'string') {
145
+ return undefined
146
+ }
147
+
148
+ const trimmed = color.trim()
149
+ if (trimmed.length === 0) {
150
+ return undefined
151
+ }
152
+
153
+ if (trimmed.toLowerCase() === 'transparent') {
154
+ return DesignHexColorUtils.resolveColorToken('transparent')
155
+ }
156
+
157
+ return parseHexString(trimmed)
158
+ ?? parseRgbString(trimmed)
159
+ ?? parseHslString(trimmed)
160
+ }
161
+
162
+ const tryParseColorValue = (color: ColorValue | number): HexColorToken | undefined => {
163
+ const fromLiteral = parseColorLiteral(color)
164
+ if (fromLiteral !== undefined) {
165
+ return fromLiteral
166
+ }
167
+
168
+ const processed = processColor(color)
169
+ if (typeof processed === 'number') {
170
+ return argbNumberToHex(processed)
171
+ }
172
+
173
+ if (typeof processed === 'string' && processed !== color) {
174
+ return parseColorLiteral(processed)
175
+ }
176
+
177
+ return undefined
178
+ }
179
+
180
+ const parseColorValue = (color: ColorValue | number): HexColorToken => {
181
+ const parsed = tryParseColorValue(color)
182
+ if (parsed === undefined) {
183
+ throw new Error(`Unable to parse ColorValue to hex: ${String(color)}`)
184
+ }
185
+
186
+ return parsed
187
+ }
188
+
189
+ export const HexColorUtils = {
190
+ ...DesignHexColorUtils,
191
+ tryParseColorValue,
192
+ parseColorValue,
193
+ }
@@ -1,2 +1,3 @@
1
+ export * from './hex'
1
2
  export * from './hitBoxOverlay'
2
3
  export * from './minimumTouchTargetHitSlop'