@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.
- package/package.json +6 -2
- package/src/components/chat/ChatAttachmentMessageBubble.tsx +6 -6
- package/src/components/chat/ChatConversationRow.tsx +28 -31
- package/src/components/chat/ChatMessageComposer.tsx +3 -2
- package/src/components/chat/ChatQuickReplyChip.tsx +6 -10
- package/src/components/chat/ChatThreadHeader.tsx +8 -8
- package/src/components/user-interaction/Button.tsx +7 -12
- package/src/components/user-interaction/IconButton.tsx +7 -12
- package/src/components/user-interaction/MultiSelect.tsx +8 -5
- package/src/components/user-interaction/Select.tsx +14 -15
- package/src/components/user-interaction/ThemedPressable.tsx +7 -10
- package/src/components/visualization-and-display/Avatar.tsx +8 -8
- package/src/components/visualization-and-display/Chip.tsx +1 -0
- package/src/components/visualization-and-display/ThemedIcon.tsx +13 -8
- package/src/components/visualization-and-display/ThemedText.tsx +9 -10
- package/src/global-contexts/content-theme/ContentThemeContext.ts +9 -6
- package/src/global-contexts/content-theme/ContentThemeProvider.tsx +46 -15
- package/src/global-contexts/theme/ThemeProvider.tsx +1 -0
- package/src/icons/types.ts +2 -1
- package/src/theme/adapters/style-adapter-utils.ts +40 -10
- package/src/theme/resolvers/chat/conversation-row.ts +5 -11
- package/src/theme/resolvers/input.ts +1 -1
- package/src/theme/resolvers/searchBar.ts +1 -1
- package/src/theme/types/resolver.ts +3 -3
- package/src/theme/types/semantics.ts +3 -3
- package/src/utils/hex.ts +193 -0
- 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?:
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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,15 +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
|
-
|
|
12
|
-
|
|
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
|
|
40
|
+
type ColorOverride =
|
|
36
41
|
| ColorValue
|
|
37
|
-
| ((prev:
|
|
42
|
+
| ((prev: HexColorToken) => ColorValue)
|
|
38
43
|
|
|
39
44
|
type TextStyleOverride =
|
|
40
45
|
| TextStyle
|
|
41
|
-
| ((prev: TextStyle, foreground:
|
|
46
|
+
| ((prev: TextStyle, foreground: HexColorToken) => TextStyle)
|
|
42
47
|
|
|
43
48
|
type IconStyleOverride =
|
|
44
49
|
| IconStyle
|
|
45
|
-
| ((prev: IconStyle, foreground:
|
|
50
|
+
| ((prev: IconStyle, foreground: HexColorToken) => IconStyle)
|
|
46
51
|
|
|
47
52
|
export type ContentThemeOverrideProviderProps = PropsWithChildren & {
|
|
48
|
-
foreground?:
|
|
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
|
|
63
|
-
|
|
64
|
-
: foregroundOverride ?? parent.foreground
|
|
85
|
+
const newForeground = resolveColorOverride(foregroundOverride, parent.foreground)
|
|
86
|
+
const newBackground = resolveColorOverride(backgroundOverride, parent.background)
|
|
65
87
|
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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,
|
package/src/icons/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 '
|
|
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 '
|
|
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 | ((
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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