@helpwave/hightide-native 0.0.5 → 0.0.7

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 (61) hide show
  1. package/package.json +4 -3
  2. package/src/components/chat/ChatAttachmentCard.tsx +4 -4
  3. package/src/components/chat/ChatConversationList.tsx +3 -3
  4. package/src/components/chat/ChatConversationRow.tsx +3 -3
  5. package/src/components/chat/ChatDateDivider.tsx +3 -3
  6. package/src/components/chat/ChatMessageBubble.tsx +3 -3
  7. package/src/components/chat/ChatMessageCard.tsx +4 -4
  8. package/src/components/chat/ChatMessageComposer.tsx +4 -4
  9. package/src/components/chat/ChatMessageList.tsx +3 -3
  10. package/src/components/chat/ChatQuickReplyChip.tsx +3 -3
  11. package/src/components/chat/ChatSystemLine.tsx +4 -4
  12. package/src/components/chat/ChatThreadHeader.tsx +3 -3
  13. package/src/components/menu/Menu.tsx +3 -3
  14. package/src/components/menu/MenuActionItem.tsx +3 -3
  15. package/src/components/menu/MenuItem.tsx +3 -3
  16. package/src/components/menu/MenuNavigationItem.tsx +3 -3
  17. package/src/components/user-interaction/Button.tsx +4 -4
  18. package/src/components/user-interaction/Checkbox.tsx +5 -5
  19. package/src/components/user-interaction/IconButton.tsx +5 -5
  20. package/src/components/user-interaction/Input.tsx +4 -4
  21. package/src/components/user-interaction/MultiSelect.tsx +6 -6
  22. package/src/components/user-interaction/Select.tsx +4 -4
  23. package/src/components/visualization-and-display/Avatar.tsx +286 -0
  24. package/src/components/visualization-and-display/Chip.tsx +4 -4
  25. package/src/components/visualization-and-display/Icon.tsx +1 -1
  26. package/src/components/visualization-and-display/index.ts +1 -0
  27. package/src/global-contexts/HightideProvider.tsx +5 -5
  28. package/src/global-contexts/hightide-config/HightideConfigUtils.ts +4 -4
  29. package/src/global-contexts/localization/LocalizationProvider.tsx +2 -2
  30. package/src/global-contexts/theme/ThemeContext.ts +3 -3
  31. package/src/global-contexts/theme/ThemeProvider.tsx +7 -7
  32. package/src/theme/resolvers/avatar.ts +231 -0
  33. package/src/theme/resolvers/button.ts +6 -6
  34. package/src/theme/resolvers/chat.ts +6 -6
  35. package/src/theme/resolvers/checkbox.ts +4 -4
  36. package/src/theme/resolvers/chip.ts +6 -6
  37. package/src/theme/resolvers/coloring.ts +4 -4
  38. package/src/theme/resolvers/iconButton.ts +6 -6
  39. package/src/theme/resolvers/index.ts +1 -0
  40. package/src/theme/resolvers/input.ts +4 -4
  41. package/src/theme/resolvers/menu.ts +4 -4
  42. package/src/theme/resolvers/multiSelect.ts +4 -4
  43. package/src/theme/resolvers/select.ts +4 -4
  44. package/src/theme/themes/{createTheme.ts → createHightideTheme.ts} +16 -14
  45. package/src/theme/themes/hightideThemes.ts +14 -0
  46. package/src/theme/themes/index.ts +2 -2
  47. package/src/theme/types/components/avatar.ts +77 -0
  48. package/src/theme/types/components/button.ts +2 -2
  49. package/src/theme/types/components/chat.ts +3 -3
  50. package/src/theme/types/components/checkbox.ts +2 -2
  51. package/src/theme/types/components/chip.ts +2 -2
  52. package/src/theme/types/components/hightide.ts +12 -10
  53. package/src/theme/types/components/iconButton.ts +3 -3
  54. package/src/theme/types/components/index.ts +1 -0
  55. package/src/theme/types/components/input.ts +2 -2
  56. package/src/theme/types/components/menu.ts +2 -2
  57. package/src/theme/types/components/multiSelect.ts +3 -3
  58. package/src/theme/types/components/select.ts +2 -2
  59. package/src/theme/types/layout.ts +12 -0
  60. package/src/theme/types/theme.ts +20 -11
  61. package/src/theme/themes/nativeThemes.ts +0 -16
@@ -0,0 +1,286 @@
1
+ import {
2
+ useEffect,
3
+ useMemo,
4
+ useState,
5
+ type ComponentType,
6
+ type ReactNode
7
+ } from 'react'
8
+ import {
9
+ Image,
10
+ Text,
11
+ View,
12
+ type ImageProps,
13
+ type StyleProp,
14
+ type ViewProps,
15
+ type ViewStyle
16
+ } from 'react-native'
17
+ import { User } from 'lucide-react-native'
18
+
19
+ import type { ElementSize } from '@helpwave/hightide-design/types'
20
+
21
+ import { useTheme } from '../../global-contexts/theme/ThemeContext'
22
+ import type {
23
+ AvatarGroupMoreStyle,
24
+ AvatarGroupStackStyle,
25
+ AvatarImageStyle,
26
+ AvatarIconStyle,
27
+ AvatarStatus,
28
+ AvatarStyle,
29
+ AvatarTextStyle,
30
+ AvatarWithLabelContainerStyle,
31
+ AvatarWithLabelTextStyle,
32
+ AvatarWithStatusContainerStyle,
33
+ AvatarStatusDotStyle
34
+ } from '../../theme/types/components/avatar'
35
+ import type { StyleOverwrite } from '../../theme/types/resolver'
36
+
37
+ export type AvatarSize = ElementSize
38
+
39
+ type ImageConfig = {
40
+ avatarUrl: string,
41
+ alt: string,
42
+ }
43
+
44
+ export type AvatarImageProps = Omit<ImageProps, 'source'> & {
45
+ source: { uri: string },
46
+ alt?: string,
47
+ }
48
+
49
+ const DefaultAvatarImage: ComponentType<AvatarImageProps> = ({ alt, ...props }) => (
50
+ <Image
51
+ {...props}
52
+ accessibilityLabel={alt}
53
+ />
54
+ )
55
+
56
+ export const AvatarUtil = {
57
+ sizes: ['xs', 'sm', 'md', 'lg'] as const satisfies readonly ElementSize[],
58
+ statuses: ['online', 'offline', 'away', 'busy', 'unknown'] as const satisfies readonly AvatarStatus[],
59
+ }
60
+
61
+ export type AvatarProps = Omit<ViewProps, 'children' | 'style'> & {
62
+ image?: ImageConfig,
63
+ name?: string,
64
+ size?: AvatarSize,
65
+ ImageComponent?: ComponentType<AvatarImageProps>,
66
+ style?: StyleProp<ViewStyle>,
67
+ avatarStyle?: StyleOverwrite<{ size?: ElementSize, isGrouped?: boolean, groupIndex?: number }, AvatarStyle>,
68
+ imageStyle?: StyleOverwrite<{ size?: ElementSize, isGrouped?: boolean, groupIndex?: number }, AvatarImageStyle>,
69
+ textStyle?: StyleOverwrite<{ size?: ElementSize, isGrouped?: boolean, groupIndex?: number }, AvatarTextStyle>,
70
+ iconStyle?: StyleOverwrite<{ size?: ElementSize, isGrouped?: boolean, groupIndex?: number }, AvatarIconStyle>,
71
+ isGrouped?: boolean,
72
+ groupIndex?: number,
73
+ }
74
+
75
+ export const Avatar = ({
76
+ image: initialImage,
77
+ name,
78
+ size = 'md',
79
+ ImageComponent = DefaultAvatarImage,
80
+ style,
81
+ avatarStyle,
82
+ imageStyle,
83
+ textStyle,
84
+ iconStyle,
85
+ isGrouped = false,
86
+ groupIndex,
87
+ ...props
88
+ }: AvatarProps) => {
89
+ const { theme } = useTheme()
90
+ const [hasError, setHasError] = useState(false)
91
+ const [hasLoaded, setHasLoaded] = useState(false)
92
+ const [image, setImage] = useState(initialImage)
93
+ const ImageElement = ImageComponent
94
+
95
+ const state = useMemo(() => ({
96
+ size,
97
+ isGrouped,
98
+ groupIndex,
99
+ }), [size, isGrouped, groupIndex])
100
+
101
+ const displayName = useMemo(() => {
102
+ const maxLetters = size === 'sm' ? 1 : 2
103
+ return (name ?? '')
104
+ .split(' ')
105
+ .filter((_, index) => index < maxLetters)
106
+ .map(value => value[0])
107
+ .join('')
108
+ .toUpperCase()
109
+ }, [name, size])
110
+
111
+ const isShowingImage = !!image && (!hasError || !hasLoaded)
112
+ const isShowingFallback = !hasLoaded || hasError
113
+
114
+ useEffect(() => {
115
+ if (initialImage?.avatarUrl !== image?.avatarUrl) {
116
+ setHasError(false)
117
+ setHasLoaded(false)
118
+ }
119
+ setImage(initialImage)
120
+ }, [image?.avatarUrl, initialImage])
121
+
122
+ const resolvedAvatar = theme.components.avatar.avatar(state, avatarStyle)
123
+ const resolvedImage = theme.components.avatar.image(state, imageStyle)
124
+ const resolvedText = theme.components.avatar.text(state, textStyle)
125
+ const resolvedIcon = theme.components.avatar.icon(state, iconStyle)
126
+
127
+ return (
128
+ <View
129
+ {...props}
130
+ style={[resolvedAvatar, style]}
131
+ >
132
+ {isShowingImage && (
133
+ <ImageElement
134
+ key={image?.avatarUrl}
135
+ source={{ uri: image?.avatarUrl ?? '' }}
136
+ alt={image?.alt}
137
+ style={[resolvedImage, { opacity: hasLoaded && !hasError ? 1 : 0 }]}
138
+ onLoad={() => setHasLoaded(true)}
139
+ onError={() => setHasError(true)}
140
+ />
141
+ )}
142
+ {isShowingFallback && (
143
+ name ? (
144
+ <Text style={resolvedText}>{displayName}</Text>
145
+ ) : (
146
+ <User
147
+ size={resolvedIcon.size}
148
+ strokeWidth={resolvedIcon.strokeWidth}
149
+ color={resolvedIcon.color}
150
+ />
151
+ )
152
+ )}
153
+ </View>
154
+ )
155
+ }
156
+
157
+ export type AvatarGroupProps = Omit<ViewProps, 'children' | 'style'> & {
158
+ avatars: Omit<AvatarProps, 'size' | 'isGrouped' | 'groupIndex'>[],
159
+ showTotalNumber?: boolean,
160
+ size?: AvatarSize,
161
+ ImageComponent?: ComponentType<AvatarImageProps>,
162
+ style?: StyleProp<ViewStyle>,
163
+ stackStyle?: StyleOverwrite<{ size?: ElementSize, count?: number }, AvatarGroupStackStyle>,
164
+ moreStyle?: StyleOverwrite<{ size?: ElementSize, count?: number }, AvatarGroupMoreStyle>,
165
+ }
166
+
167
+ export const AvatarGroup = ({
168
+ avatars,
169
+ showTotalNumber = true,
170
+ size = 'md',
171
+ ImageComponent,
172
+ style,
173
+ stackStyle,
174
+ moreStyle,
175
+ ...props
176
+ }: AvatarGroupProps) => {
177
+ const { theme } = useTheme()
178
+ const maxShownProfiles = theme.layout.avatarGroup.maxShown
179
+ const displayedProfiles = avatars.length < maxShownProfiles ? avatars : avatars.slice(0, maxShownProfiles)
180
+ const notDisplayedProfiles = avatars.length - maxShownProfiles
181
+
182
+ const state = useMemo(() => ({
183
+ size,
184
+ count: displayedProfiles.length,
185
+ }), [size, displayedProfiles.length])
186
+
187
+ const resolvedContainer = theme.components.avatar.group.container(state)
188
+ const resolvedStack = theme.components.avatar.group.stack(state, stackStyle)
189
+ const resolvedMore = theme.components.avatar.group.more(state, moreStyle)
190
+
191
+ return (
192
+ <View
193
+ {...props}
194
+ style={[resolvedContainer, style]}
195
+ >
196
+ <View style={resolvedStack}>
197
+ {displayedProfiles.map((avatar, index) => (
198
+ <Avatar
199
+ {...avatar}
200
+ key={index}
201
+ size={size}
202
+ isGrouped
203
+ groupIndex={index}
204
+ ImageComponent={avatar.ImageComponent ?? ImageComponent}
205
+ />
206
+ ))}
207
+ </View>
208
+ {showTotalNumber && notDisplayedProfiles > 0 && (
209
+ <Text style={resolvedMore}>
210
+ {`+ ${notDisplayedProfiles}`}
211
+ </Text>
212
+ )}
213
+ </View>
214
+ )
215
+ }
216
+
217
+ export type { AvatarStatus }
218
+
219
+ export type AvatarWithStatusProps = AvatarProps & {
220
+ status?: AvatarStatus,
221
+ containerStyle?: StyleOverwrite<{ size?: ElementSize, status?: AvatarStatus }, AvatarWithStatusContainerStyle>,
222
+ statusDotStyle?: StyleOverwrite<{ size?: ElementSize, status?: AvatarStatus }, AvatarStatusDotStyle>,
223
+ }
224
+
225
+ export const AvatarWithStatus = ({
226
+ status = 'unknown',
227
+ size = 'md',
228
+ style,
229
+ containerStyle,
230
+ statusDotStyle,
231
+ ...avatarProps
232
+ }: AvatarWithStatusProps) => {
233
+ const { theme } = useTheme()
234
+
235
+ const state = useMemo(() => ({
236
+ size,
237
+ status,
238
+ }), [size, status])
239
+
240
+ const resolvedContainer = theme.components.avatar.withStatus.container(state, containerStyle)
241
+ const resolvedStatusDot = theme.components.avatar.withStatus.statusDot(state, statusDotStyle)
242
+
243
+ return (
244
+ <View style={[resolvedContainer, style]}>
245
+ <Avatar {...avatarProps} size={size} />
246
+ <View style={resolvedStatusDot} />
247
+ </View>
248
+ )
249
+ }
250
+
251
+ type AvatarWithLabelPosition = 'left' | 'right'
252
+
253
+ export type AvatarWithLabelProps = AvatarProps & {
254
+ label: ReactNode,
255
+ labelPosition?: AvatarWithLabelPosition,
256
+ containerStyle?: StyleOverwrite<{ size?: ElementSize }, AvatarWithLabelContainerStyle>,
257
+ labelStyle?: StyleOverwrite<{ size?: ElementSize }, AvatarWithLabelTextStyle>,
258
+ }
259
+
260
+ export const AvatarWithLabel = ({
261
+ label,
262
+ labelPosition = 'left',
263
+ size = 'md',
264
+ style,
265
+ containerStyle,
266
+ labelStyle,
267
+ ...avatarProps
268
+ }: AvatarWithLabelProps) => {
269
+ const { theme } = useTheme()
270
+
271
+ const state = useMemo(() => ({ size }), [size])
272
+ const resolvedContainer = theme.components.avatar.withLabel.container(state, containerStyle)
273
+ const resolvedLabel = theme.components.avatar.withLabel.text(state, labelStyle)
274
+
275
+ const avatar = <Avatar {...avatarProps} size={size} />
276
+ const labelElement = typeof label === 'string' || typeof label === 'number'
277
+ ? <Text style={resolvedLabel}>{label}</Text>
278
+ : label
279
+
280
+ return (
281
+ <View style={[resolvedContainer, style]}>
282
+ {labelPosition === 'left' ? labelElement : avatar}
283
+ {labelPosition === 'left' ? avatar : labelElement}
284
+ </View>
285
+ )
286
+ }
@@ -13,19 +13,19 @@ import {
13
13
  import {
14
14
  coloringTypes,
15
15
  type ColoringType
16
- } from '@helpwave/hightide-design/helpers'
16
+ } from '@helpwave/hightide-design/utils'
17
17
  import type {
18
18
  ChipColoringStyle,
19
19
  ElementSize
20
20
  } from '@helpwave/hightide-design/types'
21
21
 
22
- import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
22
+ import { useTheme } from '../../global-contexts/theme/ThemeContext'
23
23
  import type {
24
24
  ChipState,
25
25
  ChipStyle,
26
26
  ChipTextStyle
27
- } from '@/src/theme/types/components/chip'
28
- import type { StyleOverwrite } from '@/src/theme/types/resolver'
27
+ } from '../../theme/types/components/chip'
28
+ import type { StyleOverwrite } from '../../theme/types/resolver'
29
29
 
30
30
  export type ChipSize = ElementSize
31
31
 
@@ -2,7 +2,7 @@ import type { LucideIcon } from 'lucide-react-native'
2
2
 
3
3
  import type { ElementSize } from '@helpwave/hightide-design/types'
4
4
 
5
- import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
5
+ import { useTheme } from '../../global-contexts/theme/ThemeContext'
6
6
 
7
7
  export type IconProps = {
8
8
  icon: LucideIcon,
@@ -1,2 +1,3 @@
1
+ export * from './Avatar'
1
2
  export * from './Chip'
2
3
  export * from './Icon'
@@ -7,20 +7,20 @@ import { useLocalization } from '@helpwave/hightide-utils/context/localization'
7
7
  import { hightideTranslation } from '@helpwave/hightide-utils/i18n'
8
8
  import { ArrayUtil } from '@helpwave/hightide-utils/utils'
9
9
 
10
- import { HightideContext } from '@/src/global-contexts/HightideContext'
10
+ import { HightideContext } from './HightideContext'
11
11
  import {
12
12
  LocalizationProvider,
13
13
  type LocalizationProviderProps
14
- } from '@/src/global-contexts/localization/LocalizationProvider'
15
- import { useTheme } from '@/src/global-contexts/theme/ThemeContext'
14
+ } from './localization/LocalizationProvider'
15
+ import { useTheme } from './theme/ThemeContext'
16
16
  import {
17
17
  ThemeProvider,
18
18
  type ThemeProviderProps
19
- } from '@/src/global-contexts/theme/ThemeProvider'
19
+ } from './theme/ThemeProvider'
20
20
  import {
21
21
  TranslationProvider,
22
22
  type TranslationProviderProps
23
- } from '@/src/global-contexts/translation/forward-exports'
23
+ } from './translation/forward-exports'
24
24
 
25
25
  export type HightideProviderProps = PropsWithChildren & {
26
26
  theme?: Omit<ThemeProviderProps, 'children'>,
@@ -1,8 +1,8 @@
1
1
  import type { SupportedThemesConfig } from '@helpwave/hightide-utils/context'
2
2
 
3
- import type { SupportedLocalesConfig } from '@/src/global-contexts/localization/forward-exports'
4
- import { themes } from '@/src/theme/themes/nativeThemes'
5
- import type { Theme } from '@/src/theme/types/theme'
3
+ import type { SupportedLocalesConfig } from '../localization/forward-exports'
4
+ import { themes } from '../../theme/themes/hightideThemes'
5
+ import type { HightideTheme } from '../../theme/types/theme'
6
6
 
7
7
  export const HightideConfigUtils = {
8
8
  defaultSupportedLocales: {
@@ -24,5 +24,5 @@ export const HightideConfigUtils = {
24
24
  },
25
25
  theme: themes.dark,
26
26
  },
27
- } as const satisfies SupportedThemesConfig<Theme>,
27
+ } as const satisfies SupportedThemesConfig<HightideTheme>,
28
28
  }
@@ -5,8 +5,8 @@ import {
5
5
  type LocalizationProviderProps as LocalizationProviderPropsBase
6
6
  } from '@helpwave/hightide-utils/context/localization'
7
7
 
8
- import { HightideConfigUtils } from '@/src/global-contexts/hightide-config/HightideConfigUtils'
9
- import { useNativeKeyValueStore } from '@/src/hooks/useNativeKeyValueStore'
8
+ import { HightideConfigUtils } from '../hightide-config/HightideConfigUtils'
9
+ import { useNativeKeyValueStore } from '../../hooks/useNativeKeyValueStore'
10
10
 
11
11
  export type LocalizationProviderProps = PropsWithChildren
12
12
  & Omit<LocalizationProviderPropsBase, 'store' | 'fallbackLocale' | 'supportedLocales'>
@@ -6,12 +6,12 @@ import {
6
6
  import type {
7
7
  ThemeConfigValue,
8
8
  ThemeInformation
9
- } from '@/src/global-contexts/theme/forward-exports'
10
- import type { Theme } from '@/src/theme/types/theme'
9
+ } from './forward-exports'
10
+ import type { HightideTheme } from '../../theme/types/theme'
11
11
 
12
12
  export type { ThemeInformation }
13
13
 
14
- export type ThemeContextValue = ThemeConfigValue<Theme> & {
14
+ export type ThemeContextValue = ThemeConfigValue<HightideTheme> & {
15
15
  isInitialized: boolean,
16
16
  }
17
17
 
@@ -8,19 +8,19 @@ import { Appearance } from 'react-native'
8
8
 
9
9
  import type { SupportedThemesConfig } from '@helpwave/hightide-utils/context'
10
10
 
11
- import { HightideConfigUtils } from '@/src/global-contexts/hightide-config/HightideConfigUtils'
11
+ import { HightideConfigUtils } from '../hightide-config/HightideConfigUtils'
12
12
  import {
13
13
  ThemeContext,
14
14
  useTheme,
15
15
  type ThemeContextValue
16
- } from '@/src/global-contexts/theme/ThemeContext'
16
+ } from './ThemeContext'
17
17
  import {
18
18
  useCreateThemeConfig,
19
19
  type SystemTheme
20
- } from '@/src/global-contexts/theme/forward-exports'
21
- import { useNativeKeyValueStore } from '@/src/hooks/useNativeKeyValueStore'
22
- import type { ThemeMode } from '@/src/theme/themes/nativeThemes'
23
- import type { Theme } from '@/src/theme/types/theme'
20
+ } from './forward-exports'
21
+ import { useNativeKeyValueStore } from '../../hooks/useNativeKeyValueStore'
22
+ import type { ThemeMode } from '../../theme/themes/hightideThemes'
23
+ import type { HightideTheme } from '../../theme/types/theme'
24
24
 
25
25
  export type ThemeProviderProps<T> = PropsWithChildren & {
26
26
  theme?: ThemeMode | null,
@@ -66,7 +66,7 @@ export const ThemeProvider = ({
66
66
  fallbackTheme = 'light',
67
67
  supportedThemes = HightideConfigUtils.defaultSupportedThemes,
68
68
  onChangedTheme,
69
- }: ThemeProviderProps<Theme>) => {
69
+ }: ThemeProviderProps<HightideTheme>) => {
70
70
  const store = useNativeKeyValueStore()
71
71
  const systemTheme = useNativeSystemTheme()
72
72
 
@@ -0,0 +1,231 @@
1
+ import type {
2
+ ImageStyle,
3
+ TextStyle,
4
+ ViewStyle
5
+ } from 'react-native'
6
+
7
+ import {
8
+ componentLayouts,
9
+ fontWeights
10
+ } from '@helpwave/hightide-design/tokens'
11
+ import type {
12
+ ElementSize,
13
+ HightideThemeTokens as DesignTokensTheme,
14
+ TypographyTokens
15
+ } from '@helpwave/hightide-design/types'
16
+
17
+ import { resolveColoringStyles } from './coloring'
18
+ import type {
19
+ ColorPalette,
20
+ HightideSemanticColors
21
+ } from '../types/color'
22
+ import type {
23
+ AvatarGroupState,
24
+ AvatarIconStyle,
25
+ AvatarState,
26
+ AvatarStatus,
27
+ AvatarTheme,
28
+ AvatarWithLabelState,
29
+ AvatarWithStatusState
30
+ } from '../types/components/avatar'
31
+ import type { HightideComponentThemes } from '../types/components/hightide'
32
+ import { createStyleResolver } from '../types/resolver'
33
+
34
+ const avatarFontWeights: Record<ElementSize, string> = {
35
+ xs: fontWeights.semibold,
36
+ sm: fontWeights.semibold,
37
+ md: fontWeights.semibold,
38
+ lg: fontWeights.bold,
39
+ }
40
+
41
+ const statusColor = (
42
+ status: AvatarStatus,
43
+ semantic: HightideSemanticColors,
44
+ gray: ColorPalette
45
+ ): string => {
46
+ switch (status) {
47
+ case 'online':
48
+ return semantic.positive
49
+ case 'busy':
50
+ return semantic.negative
51
+ case 'away':
52
+ return semantic.warning
53
+ case 'offline':
54
+ case 'unknown':
55
+ default:
56
+ return gray[500]
57
+ }
58
+ }
59
+
60
+ export type CreateAvatarThemeOptions = {
61
+ semantic: HightideSemanticColors,
62
+ coloring: HightideComponentThemes['coloring'],
63
+ gray: ColorPalette,
64
+ typography: TypographyTokens,
65
+ }
66
+
67
+ export const createAvatarTheme = ({
68
+ semantic,
69
+ coloring,
70
+ gray,
71
+ typography,
72
+ }: CreateAvatarThemeOptions): AvatarTheme => {
73
+ const resolveAvatar = (state: AvatarState) => {
74
+ const size = state.size ?? 'md'
75
+ const layout = componentLayouts.avatar[size]
76
+ const groupLayout = componentLayouts.avatarGroup
77
+ const resolved = resolveColoringStyles(coloring.primary, 'solid', semantic)
78
+ const borderRadius = layout.size / 2
79
+
80
+ const avatar: ViewStyle = {
81
+ position: state.isGrouped ? 'absolute' : 'relative',
82
+ flexDirection: 'row',
83
+ alignItems: 'center',
84
+ justifyContent: 'center',
85
+ width: layout.size,
86
+ height: layout.size,
87
+ padding: layout.padding,
88
+ borderRadius,
89
+ backgroundColor: resolved.backgroundColor,
90
+ overflow: 'hidden',
91
+ ...(state.isGrouped ? {
92
+ left: (state.groupIndex ?? 0) * layout.size * groupLayout.overlap,
93
+ zIndex: groupLayout.maxShown - (state.groupIndex ?? 0),
94
+ shadowColor: '#000000',
95
+ shadowOffset: { width: 4, height: 0 },
96
+ shadowOpacity: 0.2,
97
+ shadowRadius: 4,
98
+ elevation: 4,
99
+ } : {}),
100
+ }
101
+
102
+ const image: ImageStyle = {
103
+ position: 'absolute',
104
+ left: 0,
105
+ top: 0,
106
+ width: layout.size,
107
+ height: layout.size,
108
+ borderRadius,
109
+ }
110
+
111
+ const text: TextStyle = {
112
+ color: resolved.color,
113
+ fontSize: layout.fontSize,
114
+ fontWeight: avatarFontWeights[size] as TextStyle['fontWeight'],
115
+ textAlign: 'center',
116
+ }
117
+
118
+ const icon: AvatarIconStyle = {
119
+ size: componentLayouts.icon[size].size,
120
+ strokeWidth: componentLayouts.icon[size].strokeWidth,
121
+ color: resolved.color,
122
+ }
123
+
124
+ return { avatar, image, text, icon }
125
+ }
126
+
127
+ const resolveWithStatus = (state: AvatarWithStatusState) => {
128
+ const size = state.size ?? 'md'
129
+ const status = state.status ?? 'unknown'
130
+ const layout = componentLayouts.avatar[size]
131
+
132
+ const container: ViewStyle = {
133
+ position: 'relative',
134
+ alignSelf: 'flex-start',
135
+ }
136
+
137
+ const statusDot: ViewStyle = {
138
+ position: 'absolute',
139
+ right: 0,
140
+ bottom: 0,
141
+ zIndex: 1,
142
+ width: layout.statusDotSize,
143
+ height: layout.statusDotSize,
144
+ borderRadius: layout.statusDotSize / 2,
145
+ borderWidth: layout.statusDotBorderWidth,
146
+ borderColor: semantic.background,
147
+ backgroundColor: statusColor(status, semantic, gray),
148
+ }
149
+
150
+ return { container, statusDot }
151
+ }
152
+
153
+ const resolveWithLabel = (_state: AvatarWithLabelState) => {
154
+ const bodyMedium = typography.scales.body.medium
155
+
156
+ const container: ViewStyle = {
157
+ flexDirection: 'row',
158
+ alignItems: 'center',
159
+ gap: componentLayouts.avatarGroup.gap,
160
+ }
161
+
162
+ const text: TextStyle = {
163
+ fontSize: bodyMedium.fontSize,
164
+ fontWeight: bodyMedium.fontWeight as TextStyle['fontWeight'],
165
+ lineHeight: bodyMedium.lineHeight,
166
+ color: semantic.onBackground,
167
+ flexShrink: 1,
168
+ }
169
+
170
+ return { container, text }
171
+ }
172
+
173
+ const resolveGroup = (state: AvatarGroupState) => {
174
+ const size = state.size ?? 'md'
175
+ const layout = componentLayouts.avatar[size]
176
+ const groupLayout = componentLayouts.avatarGroup
177
+ const visibleCount = Math.min(state.count ?? groupLayout.maxShown, groupLayout.maxShown)
178
+ const stackWidth = layout.size * (groupLayout.overlap * Math.max(visibleCount - 1, 0) + 1)
179
+
180
+ const container: ViewStyle = {
181
+ flexDirection: 'row',
182
+ alignItems: 'center',
183
+ gap: groupLayout.gap,
184
+ height: layout.size,
185
+ alignSelf: 'flex-start',
186
+ }
187
+
188
+ const stack: ViewStyle = {
189
+ position: 'relative',
190
+ width: stackWidth,
191
+ height: layout.size,
192
+ }
193
+
194
+ const more: TextStyle = {
195
+ fontSize: (layout.size * 2) / 3,
196
+ color: semantic.onBackground,
197
+ flexShrink: 1,
198
+ }
199
+
200
+ return { container, stack, more }
201
+ }
202
+
203
+ return {
204
+ avatar: createStyleResolver((state) => resolveAvatar(state).avatar),
205
+ image: createStyleResolver((state) => resolveAvatar(state).image),
206
+ text: createStyleResolver((state) => resolveAvatar(state).text),
207
+ icon: createStyleResolver((state) => resolveAvatar(state).icon),
208
+ withStatus: {
209
+ container: createStyleResolver((state) => resolveWithStatus(state).container),
210
+ statusDot: createStyleResolver((state) => resolveWithStatus(state).statusDot),
211
+ },
212
+ withLabel: {
213
+ container: createStyleResolver((state) => resolveWithLabel(state).container),
214
+ text: createStyleResolver((state) => resolveWithLabel(state).text),
215
+ },
216
+ group: {
217
+ container: createStyleResolver((state) => resolveGroup(state).container),
218
+ stack: createStyleResolver((state) => resolveGroup(state).stack),
219
+ more: createStyleResolver((state) => resolveGroup(state).more),
220
+ },
221
+ }
222
+ }
223
+
224
+ export const createAvatarThemeFromDesign = (theme: DesignTokensTheme): AvatarTheme => {
225
+ return createAvatarTheme({
226
+ semantic: theme.semanticColors,
227
+ coloring: theme.coloring as HightideComponentThemes['coloring'],
228
+ gray: theme.colors.gray.value as ColorPalette,
229
+ typography: theme.typography,
230
+ })
231
+ }