@helpwave/hightide-native 0.4.0 → 0.5.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.5.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.5.0",
51
+ "@helpwave/hightide-utils": "0.1.0"
48
52
  },
49
53
  "peerDependencies": {
50
54
  "@react-native-async-storage/async-storage": ">=2.0.0",
@@ -67,6 +67,8 @@ export const MultiSelect = ({
67
67
  onEditComplete,
68
68
  })
69
69
 
70
+ const isSearchVisible = showSearch && options.length >= 6
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
 
@@ -210,7 +212,7 @@ export const MultiSelect = ({
210
212
  style={resolvedMenuStyle}
211
213
  onPress={(event) => event.stopPropagation()}
212
214
  >
213
- {showSearch && (
215
+ {isSearchVisible && (
214
216
  <View style={resolvedHeaderStyle}>
215
217
  <SearchBar
216
218
  value={multiSelect.searchQuery}
@@ -260,7 +262,8 @@ export const MultiSelect = ({
260
262
  <View style={multiSelectTheme.checkbox(optionState)}>
261
263
  <ThemedIcon
262
264
  icon={HightideIconRegistry.Check}
263
- size="sm"
265
+ size={checkboxIcon.size}
266
+ strokeWidth={checkboxIcon.strokeWidth}
264
267
  color={checkboxIcon.color}
265
268
  />
266
269
  </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
@@ -69,6 +68,8 @@ export const Select = ({
69
68
  onEditComplete,
70
69
  })
71
70
 
71
+ const isSearchVisible = showSearch && options.length >= 6
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
  }}
@@ -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,
@@ -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>()
@@ -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>()
@@ -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'