@helpwave/hightide-native 0.3.2 → 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.
Files changed (27) hide show
  1. package/package.json +6 -2
  2. package/src/components/chat/ChatAttachmentMessageBubble.tsx +6 -6
  3. package/src/components/chat/ChatConversationRow.tsx +28 -31
  4. package/src/components/chat/ChatMessageComposer.tsx +3 -2
  5. package/src/components/chat/ChatQuickReplyChip.tsx +6 -10
  6. package/src/components/chat/ChatThreadHeader.tsx +8 -8
  7. package/src/components/user-interaction/Button.tsx +7 -12
  8. package/src/components/user-interaction/IconButton.tsx +7 -12
  9. package/src/components/user-interaction/MultiSelect.tsx +8 -5
  10. package/src/components/user-interaction/Select.tsx +14 -15
  11. package/src/components/user-interaction/ThemedPressable.tsx +7 -10
  12. package/src/components/visualization-and-display/Avatar.tsx +8 -8
  13. package/src/components/visualization-and-display/Chip.tsx +1 -0
  14. package/src/components/visualization-and-display/ThemedIcon.tsx +13 -8
  15. package/src/components/visualization-and-display/ThemedText.tsx +9 -10
  16. package/src/global-contexts/content-theme/ContentThemeContext.ts +9 -6
  17. package/src/global-contexts/content-theme/ContentThemeProvider.tsx +46 -15
  18. package/src/global-contexts/theme/ThemeProvider.tsx +1 -0
  19. package/src/icons/types.ts +2 -1
  20. package/src/theme/adapters/style-adapter-utils.ts +40 -10
  21. package/src/theme/resolvers/chat/conversation-row.ts +5 -11
  22. package/src/theme/resolvers/input.ts +1 -1
  23. package/src/theme/resolvers/searchBar.ts +1 -1
  24. package/src/theme/types/resolver.ts +3 -3
  25. package/src/theme/types/semantics.ts +3 -3
  26. package/src/utils/hex.ts +193 -0
  27. package/src/utils/index.ts +1 -0
@@ -1,22 +1,24 @@
1
+ import type {
2
+ ColorValue } from 'react-native'
1
3
  import {
2
4
  View,
3
5
  type ViewProps
4
6
  } from 'react-native'
5
7
 
6
- import type { HexColorToken } from '@helpwave/hightide-design/primitive-tokens'
7
8
  import type { Appearance } from '@helpwave/hightide-design/semantic-token-resolvers'
8
9
  import type { IconSize } from '@helpwave/hightide-design/theme-tokens'
9
10
 
10
11
  import type { IconComponent } from '../../icons/types'
11
12
  import { useContentTheme } from '../../global-contexts/content-theme/ContentThemeContext'
12
13
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
14
+ import { HexColorUtils } from '../../utils/hex'
13
15
 
14
16
  export type ThemedIconAppearance = Appearance
15
17
 
16
18
  export type ThemedIconProps = Omit<ViewProps, 'children'> & {
17
19
  icon: IconComponent,
18
20
  size?: IconSize | number,
19
- color?: string,
21
+ color?: ColorValue,
20
22
  strokeWidth?: number,
21
23
  appearance?: ThemedIconAppearance,
22
24
  }
@@ -31,7 +33,7 @@ export const ThemedIcon = ({
31
33
  ...props
32
34
  }: ThemedIconProps) => {
33
35
  const { theme } = useTheme()
34
- const { foreground, iconStyle } = useContentTheme()
36
+ const { foreground, background, iconStyle } = useContentTheme()
35
37
  const resolvedSize = size ?? iconStyle.size ?? 'md'
36
38
  const iconToken = typeof resolvedSize === 'number'
37
39
  ? {
@@ -40,13 +42,16 @@ export const ThemedIcon = ({
40
42
  }
41
43
  : theme.components.icon[resolvedSize]
42
44
 
43
- // TODO fix this by using a hex color parser
44
- const baseColor = (color
45
- ?? iconStyle.color
46
- ?? foreground) as HexColorToken
45
+ const baseColor = HexColorUtils.tryParseColorValue(color ?? iconStyle.color ?? foreground) ?? foreground
47
46
  const resolvedColor = appearance === 'normal'
48
47
  ? baseColor
49
- : theme.semantics.withAppearance({ color: baseColor, appearance })
48
+ : theme.semantics.withAppearance({
49
+ colorPair: {
50
+ color: background,
51
+ onColor: baseColor,
52
+ },
53
+ appearance,
54
+ })
50
55
 
51
56
  return (
52
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,15 +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
- textStyle: TextStyle,
12
- iconStyle: IconStyle,
13
- }
11
+ export type ContentThemeContextValue = Readonly<{
12
+ foreground: HexColorToken,
13
+ background: HexColorToken,
14
+ textStyle: Readonly<TextStyle>,
15
+ iconStyle: Readonly<IconStyle>,
16
+ }>
14
17
 
15
18
  export const ContentThemeContext = createContext<ContentThemeContextValue | null>(null)
16
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,
@@ -16,14 +19,16 @@ export type ContentThemeRootProviderProps = PropsWithChildren & ContentThemeCont
16
19
  export const ContentThemeRootProvider = ({
17
20
  children,
18
21
  foreground,
22
+ background,
19
23
  textStyle,
20
24
  iconStyle,
21
25
  }: ContentThemeRootProviderProps) => {
22
26
  const value = useMemo((): ContentThemeContextValue => ({
23
27
  foreground,
28
+ background,
24
29
  textStyle,
25
30
  iconStyle,
26
- }), [foreground, textStyle, iconStyle])
31
+ }), [foreground, background, textStyle, iconStyle])
27
32
 
28
33
  return (
29
34
  <ContentThemeContext.Provider value={value}>
@@ -32,51 +37,77 @@ export const ContentThemeRootProvider = ({
32
37
  )
33
38
  }
34
39
 
35
- type ForegroundOverride =
40
+ type ColorOverride =
36
41
  | ColorValue
37
- | ((prev: ColorValue) => ColorValue)
42
+ | ((prev: HexColorToken) => ColorValue)
38
43
 
39
44
  type TextStyleOverride =
40
45
  | TextStyle
41
- | ((prev: TextStyle, foreground: ColorValue) => TextStyle)
46
+ | ((prev: TextStyle, foreground: HexColorToken) => TextStyle)
42
47
 
43
48
  type IconStyleOverride =
44
49
  | IconStyle
45
- | ((prev: IconStyle, foreground: ColorValue) => IconStyle)
50
+ | ((prev: IconStyle, foreground: HexColorToken) => IconStyle)
46
51
 
47
52
  export type ContentThemeOverrideProviderProps = PropsWithChildren & {
48
- foreground?: ForegroundOverride,
53
+ foreground?: ColorOverride,
54
+ background?: ColorOverride,
49
55
  textStyle?: TextStyleOverride,
50
56
  iconStyle?: IconStyleOverride,
57
+ isKeepingTextColor?: boolean,
58
+ isKeepingIconColor?: boolean,
59
+ }
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)
51
71
  }
52
72
 
53
73
  export const ContentThemeOverrideProvider = ({
54
74
  children,
55
75
  foreground: foregroundOverride,
76
+ background: backgroundOverride,
56
77
  textStyle: textStyleOverride,
57
78
  iconStyle: iconStyleOverride,
79
+ isKeepingIconColor = false,
80
+ isKeepingTextColor = false,
58
81
  }: ContentThemeOverrideProviderProps) => {
59
82
  const parent = useContentTheme()
60
83
 
61
84
  const value = useMemo((): ContentThemeContextValue => {
62
- const foreground = typeof foregroundOverride === 'function'
63
- ? foregroundOverride(parent.foreground)
64
- : foregroundOverride ?? parent.foreground
85
+ const newForeground = resolveColorOverride(foregroundOverride, parent.foreground)
86
+ const newBackground = resolveColorOverride(backgroundOverride, parent.background)
65
87
 
66
- const textStyle = typeof textStyleOverride === 'function'
67
- ? textStyleOverride(parent.textStyle, foreground)
88
+ let textStyle = typeof textStyleOverride === 'function'
89
+ ? textStyleOverride(parent.textStyle, parent.foreground)
68
90
  : textStyleOverride ?? parent.textStyle
69
91
 
70
- const iconStyle = typeof iconStyleOverride === 'function'
71
- ? iconStyleOverride(parent.iconStyle, foreground)
92
+ if (!isKeepingTextColor && newForeground !== undefined) {
93
+ textStyle = { ...textStyle, color: newForeground }
94
+ }
95
+
96
+ let iconStyle = typeof iconStyleOverride === 'function'
97
+ ? iconStyleOverride(parent.iconStyle, parent.foreground)
72
98
  : iconStyleOverride ?? parent.iconStyle
73
99
 
100
+ if (!isKeepingIconColor && newForeground !== undefined) {
101
+ iconStyle = { ...iconStyle, color: newForeground }
102
+ }
103
+
74
104
  return {
75
- foreground,
105
+ foreground: newForeground ?? parent.foreground,
106
+ background: newBackground ?? parent.background,
76
107
  textStyle,
77
108
  iconStyle,
78
109
  }
79
- }, [parent, foregroundOverride, textStyleOverride, iconStyleOverride])
110
+ }, [parent, foregroundOverride, textStyleOverride, iconStyleOverride, backgroundOverride, isKeepingTextColor, isKeepingIconColor])
80
111
 
81
112
  return (
82
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,
@@ -1,8 +1,9 @@
1
1
  import type { ComponentType } from 'react'
2
+ import type { ColorValue } from 'react-native'
2
3
 
3
4
  export type IconStyle = {
4
5
  size?: number,
5
- color?: string,
6
+ color?: ColorValue,
6
7
  strokeWidth?: number,
7
8
  }
8
9
 
@@ -1,6 +1,8 @@
1
- import type {
2
- TextStyle,
3
- ViewStyle
1
+ import {
2
+ StyleSheet,
3
+ type StyleProp,
4
+ type TextStyle,
5
+ type ViewStyle
4
6
  } from 'react-native'
5
7
 
6
8
  import type {
@@ -24,6 +26,36 @@ import {
24
26
  import type { ShadowToken } from '@helpwave/hightide-design/theme-tokens'
25
27
  import type { IconStyle } from '../../icons'
26
28
 
29
+ function getStyleProperty<
30
+ T extends ViewStyle,
31
+ K extends keyof T
32
+ >(
33
+ style: StyleProp<T>,
34
+ property: K
35
+ ): T[K] {
36
+ if (style == null) {
37
+ return undefined as T[K]
38
+ }
39
+
40
+ if (Array.isArray(style)) {
41
+ for (let i = style.length - 1; i >= 0; i--) {
42
+ const value = getStyleProperty(
43
+ style[i] as StyleProp<T>,
44
+ property
45
+ )
46
+
47
+ if (value !== undefined) {
48
+ return value
49
+ }
50
+ }
51
+
52
+ return undefined as T[K]
53
+ }
54
+
55
+ const flattened = StyleSheet.flatten(style) as T
56
+ return flattened[property]
57
+ }
58
+
27
59
  type OptionalViewStyle<K extends keyof ViewStyle> = {
28
60
  [P in K]?: ViewStyle[P]
29
61
  }
@@ -144,7 +176,6 @@ const alignContentStyleAdapter = (
144
176
  const borderRadiusStyleAdapter = (
145
177
  borderRadius?: BorderRadiusToken
146
178
  ): OptionalViewStyle<
147
- | 'borderRadius'
148
179
  | 'borderTopLeftRadius'
149
180
  | 'borderTopRightRadius'
150
181
  | 'borderBottomLeftRadius'
@@ -156,16 +187,14 @@ const borderRadiusStyleAdapter = (
156
187
 
157
188
  if (borderRadius.type === 'all') {
158
189
  return {
159
- borderRadius: borderRadius.value,
160
- borderTopLeftRadius: undefined,
161
- borderTopRightRadius: undefined,
162
- borderBottomLeftRadius: undefined,
163
- borderBottomRightRadius: undefined,
190
+ borderTopLeftRadius: borderRadius.value,
191
+ borderTopRightRadius: borderRadius.value,
192
+ borderBottomLeftRadius: borderRadius.value,
193
+ borderBottomRightRadius: borderRadius.value,
164
194
  }
165
195
  }
166
196
 
167
197
  return {
168
- borderRadius: undefined,
169
198
  borderTopLeftRadius: borderRadius.topLeft,
170
199
  borderTopRightRadius: borderRadius.topRight,
171
200
  borderBottomLeftRadius: borderRadius.bottomLeft,
@@ -428,6 +457,7 @@ export const StyleAdapterUtils = {
428
457
  container: containerStyleAdapter,
429
458
  text: textStyleAdapter,
430
459
  icon: iconStyleAdapter,
460
+ stylePropResolver: getStyleProperty
431
461
  }
432
462
 
433
463
  export type StyleAdapters = typeof StyleAdapterUtils
@@ -67,20 +67,14 @@ export const toChatConversationRowThemeResolvers: ComponentThemeResolver<ChatCon
67
67
  const resolvePressable = (pressableState: PressableState) => componentTokens.pressable({
68
68
  themeTokens,
69
69
  semanticResolvers: semanticTokens,
70
- overrides: {
71
- size: pressableOverrides.overrides?.size,
72
- color: pressableOverrides.overrides?.color,
73
- coloringStyle: pressableOverrides.overrides?.coloringStyle,
74
- coloringColorVariant: pressableOverrides.overrides?.coloringColorVariant,
75
- hasAdditionalHorizontalPadding: pressableOverrides.overrides?.hasAdditionalHorizontalPadding,
76
- },
70
+ overrides: pressableOverrides.overrides ?? {},
77
71
  state: toPressableInteractionState(pressableState),
78
72
  })
79
73
 
80
74
  return {
81
75
  container: createStyleResolver((pressableState: PressableState): PressableContainerStyle => ({
82
76
  ...StyleAdapterUtils.container(resolvePressable(pressableState).container),
83
- ...StyleAdapterUtils.container(pressableOverrides.container = {}),
77
+ ...StyleAdapterUtils.container(pressableOverrides.container ?? {}),
84
78
  })),
85
79
  stateLayer: createStyleResolver((pressableState: PressableState): PressableStateLayerStyle => ({
86
80
  ...StyleAdapterUtils.container(resolvePressable(pressableState).stateLayer),
@@ -89,15 +83,15 @@ export const toChatConversationRowThemeResolvers: ComponentThemeResolver<ChatCon
89
83
  right: 0,
90
84
  bottom: 0,
91
85
  left: 0,
92
- ...StyleAdapterUtils.container(pressableOverrides.stateLayer = {}),
86
+ ...StyleAdapterUtils.container(pressableOverrides.stateLayer ?? {}),
93
87
  })),
94
88
  text: createStyleResolver((pressableState: PressableState): PressableTextStyle => ({
95
89
  ...StyleAdapterUtils.text(resolvePressable(pressableState).text),
96
- ...StyleAdapterUtils.text(pressableOverrides.text = {}),
90
+ ...StyleAdapterUtils.text(pressableOverrides.text ?? {}),
97
91
  })),
98
92
  icon: createValueResolver((pressableState: PressableState): PressableIconStyle => ({
99
93
  ...StyleAdapterUtils.icon(resolvePressable(pressableState).icon),
100
- ...StyleAdapterUtils.icon(pressableOverrides.icon = {}),
94
+ ...StyleAdapterUtils.icon(pressableOverrides.icon ?? {}),
101
95
  })),
102
96
  }
103
97
  }),
@@ -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>()
@@ -41,7 +41,7 @@ export const toPressableInteractionState = (
41
41
  }
42
42
 
43
43
  export type StyleOverwrite<TState, TStyle> =
44
- TStyle | ((prev: TStyle, state: TState) => TStyle)
44
+ TStyle | ((state: TState, prev: TStyle) => TStyle)
45
45
 
46
46
  export type StyleResolverFunction<TState, TStyle> = (
47
47
  props: TState,
@@ -67,7 +67,7 @@ export const createStyleResolver = <TState, TStyle>(
67
67
  }
68
68
 
69
69
  if (typeof overwrite === 'function') {
70
- return (overwrite as (prev: TStyle, state: TState) => TStyle)(base, props)
70
+ return (overwrite as (state: TState, prev: TStyle) => TStyle)(props, base)
71
71
  }
72
72
 
73
73
  return [base, overwrite] as TStyle
@@ -91,7 +91,7 @@ export const createValueResolver = <TState, TValue>(
91
91
  }
92
92
 
93
93
  if (typeof overwrite === 'function') {
94
- return (overwrite as (prev: TValue, state: TState) => TValue)(base, props)
94
+ return (overwrite as (state: TState, prev: TValue) => TValue)(props, base)
95
95
  }
96
96
 
97
97
  return overwrite
@@ -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'