@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 +7 -3
- package/src/components/chat/ChatAttachmentMessageBubble.tsx +5 -6
- package/src/components/chat/ChatMessageBubble.tsx +5 -9
- package/src/components/user-interaction/Input.tsx +23 -9
- package/src/components/user-interaction/MultiSelect.tsx +11 -7
- package/src/components/user-interaction/Select.tsx +16 -17
- package/src/components/user-interaction/Textarea.tsx +26 -0
- package/src/components/user-interaction/index.ts +1 -0
- package/src/components/visualization-and-display/ThemedIcon.tsx +10 -7
- package/src/components/visualization-and-display/ThemedText.tsx +9 -10
- package/src/global-contexts/content-theme/ContentThemeContext.ts +9 -7
- package/src/global-contexts/content-theme/ContentThemeProvider.tsx +30 -16
- package/src/global-contexts/theme/ThemeProvider.tsx +1 -0
- package/src/theme/resolvers/index.ts +1 -0
- package/src/theme/resolvers/input.ts +1 -1
- package/src/theme/resolvers/multiSelect.ts +0 -1
- package/src/theme/resolvers/searchBar.ts +1 -1
- package/src/theme/resolvers/textarea.ts +14 -0
- package/src/theme/themes/createHightideTheme.ts +6 -0
- package/src/theme/types/components/hightide.ts +2 -0
- package/src/theme/types/components/index.ts +1 -0
- package/src/theme/types/components/textarea.ts +8 -0
- package/src/theme/types/semantics.ts +3 -3
- package/src/utils/hex.ts +193 -0
- package/src/utils/index.ts +1 -0
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"license": "AGPL-3.0-only",
|
|
13
|
-
"version": "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-
|
|
47
|
-
"@helpwave/hightide-
|
|
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
|
-
|
|
140
|
+
style
|
|
141
141
|
),
|
|
142
|
-
[bubbleOverrides, state,
|
|
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={
|
|
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={{
|
|
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
|
|
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?:
|
|
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,
|
|
62
|
-
[theme, state,
|
|
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={
|
|
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
|
|
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?:
|
|
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,
|
|
112
|
-
[theme, state,
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
92
|
-
[multiSelectTheme,
|
|
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 =
|
|
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
|
-
{
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
99
|
-
[selectTheme,
|
|
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 =
|
|
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
|
-
{
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
})
|
|
@@ -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
|
-
|
|
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({
|
|
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({
|
|
33
|
-
|
|
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 {
|
|
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:
|
|
11
|
-
background:
|
|
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:
|
|
42
|
+
| ((prev: HexColorToken) => ColorValue)
|
|
40
43
|
|
|
41
44
|
type TextStyleOverride =
|
|
42
45
|
| TextStyle
|
|
43
|
-
| ((prev: TextStyle, foreground:
|
|
46
|
+
| ((prev: TextStyle, foreground: HexColorToken) => TextStyle)
|
|
44
47
|
|
|
45
48
|
type IconStyleOverride =
|
|
46
49
|
| IconStyle
|
|
47
|
-
| ((prev: IconStyle, foreground:
|
|
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:
|
|
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 =
|
|
71
|
-
|
|
72
|
-
: foregroundOverride
|
|
85
|
+
const newForeground = resolveColorOverride(foregroundOverride, parent.foreground)
|
|
86
|
+
const newBackground = resolveColorOverride(backgroundOverride, parent.background)
|
|
73
87
|
|
|
74
|
-
|
|
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)
|
|
92
|
+
if (!isKeepingTextColor && newForeground !== undefined) {
|
|
93
|
+
textStyle = { ...textStyle, color: newForeground }
|
|
94
|
+
}
|
|
83
95
|
|
|
84
|
-
|
|
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)
|
|
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,
|
|
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,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from '../types/resolver'
|
|
18
18
|
|
|
19
19
|
import { StyleAdapterUtils } from '../adapters'
|
|
20
|
-
import { HexColorUtils } from '
|
|
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 '
|
|
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,
|
|
@@ -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
|
-
|
|
64
|
+
colorPair: ColorPairToken,
|
|
65
65
|
appearance: Appearance,
|
|
66
66
|
}, HexColorToken>,
|
|
67
67
|
asFaded: BoundSemanticResolver<{
|
|
68
|
-
|
|
68
|
+
colorPair: ColorPairToken,
|
|
69
69
|
}, HexColorToken>,
|
|
70
70
|
asDescription: BoundSemanticResolver<{
|
|
71
|
-
|
|
71
|
+
colorPair: ColorPairToken,
|
|
72
72
|
}, HexColorToken>,
|
|
73
73
|
} & ElementLayoutTokens
|
package/src/utils/hex.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/index.ts
CHANGED