@helpwave/hightide-native 0.0.6 → 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 (36) hide show
  1. package/package.json +3 -3
  2. package/src/components/chat/ChatMessageCard.tsx +1 -1
  3. package/src/components/chat/ChatSystemLine.tsx +1 -1
  4. package/src/components/user-interaction/Button.tsx +1 -1
  5. package/src/components/user-interaction/IconButton.tsx +1 -1
  6. package/src/components/visualization-and-display/Avatar.tsx +286 -0
  7. package/src/components/visualization-and-display/Chip.tsx +1 -1
  8. package/src/components/visualization-and-display/index.ts +1 -0
  9. package/src/global-contexts/hightide-config/HightideConfigUtils.ts +3 -3
  10. package/src/global-contexts/theme/ThemeContext.ts +2 -2
  11. package/src/global-contexts/theme/ThemeProvider.tsx +3 -3
  12. package/src/theme/resolvers/avatar.ts +231 -0
  13. package/src/theme/resolvers/button.ts +1 -1
  14. package/src/theme/resolvers/chat.ts +2 -2
  15. package/src/theme/resolvers/checkbox.ts +1 -1
  16. package/src/theme/resolvers/chip.ts +1 -1
  17. package/src/theme/resolvers/coloring.ts +1 -1
  18. package/src/theme/resolvers/iconButton.ts +1 -1
  19. package/src/theme/resolvers/index.ts +1 -0
  20. package/src/theme/resolvers/input.ts +1 -1
  21. package/src/theme/resolvers/menu.ts +1 -1
  22. package/src/theme/resolvers/multiSelect.ts +1 -1
  23. package/src/theme/resolvers/select.ts +1 -1
  24. package/src/theme/themes/{createTheme.ts → createHightideTheme.ts} +6 -4
  25. package/src/theme/themes/hightideThemes.ts +14 -0
  26. package/src/theme/themes/index.ts +2 -2
  27. package/src/theme/types/components/avatar.ts +77 -0
  28. package/src/theme/types/components/button.ts +1 -1
  29. package/src/theme/types/components/chat.ts +1 -1
  30. package/src/theme/types/components/chip.ts +1 -1
  31. package/src/theme/types/components/hightide.ts +2 -0
  32. package/src/theme/types/components/iconButton.ts +1 -1
  33. package/src/theme/types/components/index.ts +1 -0
  34. package/src/theme/types/layout.ts +12 -0
  35. package/src/theme/types/theme.ts +15 -6
  36. package/src/theme/themes/nativeThemes.ts +0 -16
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "access": "public"
11
11
  },
12
12
  "license": "AGPL-3.0-only",
13
- "version": "0.0.6",
13
+ "version": "0.0.7",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "src"
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "lucide-react-native": "0.561.0",
46
- "@helpwave/hightide-design": "0.0.4",
47
- "@helpwave/hightide-utils": "0.0.4"
46
+ "@helpwave/hightide-design": "0.0.5",
47
+ "@helpwave/hightide-utils": "0.0.5"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@react-native-async-storage/async-storage": ">=2.0.0",
@@ -10,7 +10,7 @@ import {
10
10
  type ViewStyle
11
11
  } from 'react-native'
12
12
 
13
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
13
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
14
14
 
15
15
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
16
16
  import type {
@@ -11,7 +11,7 @@ import {
11
11
  } from 'react-native'
12
12
  import { CheckCheck } from 'lucide-react-native'
13
13
 
14
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
14
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
15
15
 
16
16
  import { useTheme } from '../../global-contexts/theme/ThemeContext'
17
17
  import type {
@@ -13,7 +13,7 @@ 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
  ButtonColoringStyle,
19
19
  ElementSize
@@ -10,7 +10,7 @@ import {
10
10
  } from 'react-native'
11
11
  import type { LucideIcon } from 'lucide-react-native'
12
12
 
13
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
13
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
14
14
  import type {
15
15
  ButtonColoringStyle,
16
16
  ElementSize
@@ -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,7 +13,7 @@ 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
@@ -1,2 +1,3 @@
1
+ export * from './Avatar'
1
2
  export * from './Chip'
2
3
  export * from './Icon'
@@ -1,8 +1,8 @@
1
1
  import type { SupportedThemesConfig } from '@helpwave/hightide-utils/context'
2
2
 
3
3
  import type { SupportedLocalesConfig } from '../localization/forward-exports'
4
- import { themes } from '../../theme/themes/nativeThemes'
5
- import type { Theme } from '../../theme/types/theme'
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
  }
@@ -7,11 +7,11 @@ import type {
7
7
  ThemeConfigValue,
8
8
  ThemeInformation
9
9
  } from './forward-exports'
10
- import type { Theme } from '../../theme/types/theme'
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
 
@@ -19,8 +19,8 @@ import {
19
19
  type SystemTheme
20
20
  } from './forward-exports'
21
21
  import { useNativeKeyValueStore } from '../../hooks/useNativeKeyValueStore'
22
- import type { ThemeMode } from '../../theme/themes/nativeThemes'
23
- import type { Theme } from '../../theme/types/theme'
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
+ }
@@ -8,7 +8,7 @@ import {
8
8
  fontWeights
9
9
  } from '@helpwave/hightide-design/tokens'
10
10
  import type {
11
- HightideDesignTokens as DesignTokensTheme,
11
+ HightideThemeTokens as DesignTokensTheme,
12
12
  ElementSize
13
13
  } from '@helpwave/hightide-design/types'
14
14
 
@@ -4,11 +4,11 @@ import {
4
4
  type ViewStyle
5
5
  } from 'react-native'
6
6
 
7
- import { hexWithAlpha } from '@helpwave/hightide-design/helpers'
7
+ import { hexWithAlpha } from '@helpwave/hightide-design/utils'
8
8
  import { fontWeights } from '@helpwave/hightide-design/tokens'
9
9
  import type {
10
10
  ComponentColorTokens,
11
- HightideDesignTokens as DesignTokensTheme
11
+ HightideThemeTokens as DesignTokensTheme
12
12
  } from '@helpwave/hightide-design/types'
13
13
 
14
14
  import type {
@@ -3,7 +3,7 @@ import type { ViewStyle } from 'react-native'
3
3
  import { componentLayouts } from '@helpwave/hightide-design/tokens'
4
4
  import type {
5
5
  ComponentColorTokens,
6
- HightideDesignTokens as DesignTokensTheme,
6
+ HightideThemeTokens as DesignTokensTheme,
7
7
  ElementSize
8
8
  } from '@helpwave/hightide-design/types'
9
9
 
@@ -7,7 +7,7 @@ import {
7
7
  componentLayouts,
8
8
  fontWeights
9
9
  } from '@helpwave/hightide-design/tokens'
10
- import type { HightideDesignTokens as DesignTokensTheme } from '@helpwave/hightide-design/types'
10
+ import type { HightideThemeTokens as DesignTokensTheme } from '@helpwave/hightide-design/types'
11
11
 
12
12
  import { resolveColoringStyles } from './coloring'
13
13
  import type { HightideSemanticColors } from '../types/color'
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  hexWithAlpha
3
- } from '@helpwave/hightide-design/helpers'
3
+ } from '@helpwave/hightide-design/utils'
4
4
  import { componentLayouts } from '@helpwave/hightide-design/tokens'
5
5
  import type {
6
6
  ButtonColoringStyle,
@@ -1,7 +1,7 @@
1
1
  import type { ViewStyle } from 'react-native'
2
2
 
3
3
  import { componentLayouts } from '@helpwave/hightide-design/tokens'
4
- import type { HightideDesignTokens as DesignTokensTheme } from '@helpwave/hightide-design/types'
4
+ import type { HightideThemeTokens as DesignTokensTheme } from '@helpwave/hightide-design/types'
5
5
 
6
6
  import {
7
7
  isOutlineColoringStyle,
@@ -1,3 +1,4 @@
1
+ export * from './avatar'
1
2
  export * from './button'
2
3
  export * from './chat'
3
4
  export * from './checkbox'
@@ -3,7 +3,7 @@ import type { TextStyle } from 'react-native'
3
3
  import { componentLayouts } from '@helpwave/hightide-design/tokens'
4
4
  import type {
5
5
  ComponentColorTokens,
6
- HightideDesignTokens as DesignTokensTheme
6
+ HightideThemeTokens as DesignTokensTheme
7
7
  } from '@helpwave/hightide-design/types'
8
8
 
9
9
  import type { HightideSemanticColors } from '../types/color'
@@ -3,7 +3,7 @@ import { StyleSheet } from 'react-native'
3
3
  import { fontWeights } from '@helpwave/hightide-design/tokens'
4
4
  import type {
5
5
  ComponentColorTokens,
6
- HightideDesignTokens as DesignTokensTheme
6
+ HightideThemeTokens as DesignTokensTheme
7
7
  } from '@helpwave/hightide-design/types'
8
8
 
9
9
  import type { HightideSemanticColors } from '../types/color'
@@ -1,7 +1,7 @@
1
1
  import { fontWeights } from '@helpwave/hightide-design/tokens'
2
2
  import type {
3
3
  ComponentColorTokens,
4
- HightideDesignTokens as DesignTokensTheme
4
+ HightideThemeTokens as DesignTokensTheme
5
5
  } from '@helpwave/hightide-design/types'
6
6
 
7
7
  import type { HightideSemanticColors } from '../types/color'
@@ -1,7 +1,7 @@
1
1
  import { fontWeights } from '@helpwave/hightide-design/tokens'
2
2
  import type {
3
3
  ComponentColorTokens,
4
- HightideDesignTokens as DesignTokensTheme
4
+ HightideThemeTokens as DesignTokensTheme
5
5
  } from '@helpwave/hightide-design/types'
6
6
 
7
7
  import type { HightideSemanticColors } from '../types/color'
@@ -1,8 +1,9 @@
1
1
  import type {
2
2
  ColorPaletteToken,
3
- HightideDesignTokens
3
+ HightideThemeTokens
4
4
  } from '@helpwave/hightide-design/types'
5
5
 
6
+ import { createAvatarThemeFromDesign } from '../resolvers/avatar'
6
7
  import { createButtonThemeFromDesign } from '../resolvers/button'
7
8
  import { createChatThemeFromDesign } from '../resolvers/chat'
8
9
  import { createCheckboxThemeFromDesign } from '../resolvers/checkbox'
@@ -17,7 +18,7 @@ import type {
17
18
  ColorPalette,
18
19
  HightideColors
19
20
  } from '../types/color'
20
- import type { Theme } from '../types/theme'
21
+ import type { HightideTheme } from '../types/theme'
21
22
 
22
23
  const unwrapColorPaletteToken = (token: ColorPaletteToken): Color | ColorPalette => {
23
24
  if (token.type === 'singleValue') {
@@ -26,7 +27,7 @@ const unwrapColorPaletteToken = (token: ColorPaletteToken): Color | ColorPalette
26
27
  return token.value
27
28
  }
28
29
 
29
- const unwrapColors = (colors: HightideDesignTokens['colors']): HightideColors & Record<string, Color | ColorPalette> => {
30
+ const unwrapColors = (colors: HightideThemeTokens['colors']): HightideColors & Record<string, Color | ColorPalette> => {
30
31
  const result: Record<string, Color | ColorPalette> = {}
31
32
  for (const [key, token] of Object.entries(colors)) {
32
33
  result[key] = unwrapColorPaletteToken(token)
@@ -34,7 +35,7 @@ const unwrapColors = (colors: HightideDesignTokens['colors']): HightideColors &
34
35
  return result as HightideColors & Record<string, Color | ColorPalette>
35
36
  }
36
37
 
37
- export const createTheme = (tokens: HightideDesignTokens): Theme => ({
38
+ export const createHightideTheme = (tokens: HightideThemeTokens): HightideTheme => ({
38
39
  colors: unwrapColors(tokens.colors),
39
40
  semantic: tokens.semanticColors,
40
41
  typography: tokens.typography,
@@ -51,5 +52,6 @@ export const createTheme = (tokens: HightideDesignTokens): Theme => ({
51
52
  multiSelect: createMultiSelectThemeFromDesign(tokens),
52
53
  chat: createChatThemeFromDesign(tokens),
53
54
  menu: createMenuThemeFromDesign(tokens),
55
+ avatar: createAvatarThemeFromDesign(tokens),
54
56
  },
55
57
  })
@@ -0,0 +1,14 @@
1
+ import { ThemeTokens as designThemes } from '@helpwave/hightide-design/tokens'
2
+ import { createHightideTheme } from './createHightideTheme'
3
+
4
+ export const lightTheme = createHightideTheme(designThemes.light)
5
+ export const darkTheme = createHightideTheme(designThemes.dark)
6
+
7
+ export const themes = {
8
+ light: lightTheme,
9
+ dark: darkTheme,
10
+ } as const
11
+
12
+ export type HightideThemeModes = 'dark' | 'light'
13
+
14
+ export type ThemeMode = string | HightideThemeModes
@@ -1,2 +1,2 @@
1
- export * from './createTheme'
2
- export * from './nativeThemes'
1
+ export * from './createHightideTheme'
2
+ export * from './hightideThemes'
@@ -0,0 +1,77 @@
1
+ import type {
2
+ ImageStyle,
3
+ TextStyle,
4
+ ViewStyle
5
+ } from 'react-native'
6
+
7
+ import type { ElementSize } from '@helpwave/hightide-design/types'
8
+
9
+ import type { StyleResolverFunction } from '../resolver'
10
+
11
+ export type AvatarStatus = 'online' | 'offline' | 'away' | 'busy' | 'unknown'
12
+
13
+ export type AvatarState = {
14
+ size?: ElementSize,
15
+ isGrouped?: boolean,
16
+ groupIndex?: number,
17
+ }
18
+
19
+ export type AvatarWithStatusState = {
20
+ size?: ElementSize,
21
+ status?: AvatarStatus,
22
+ }
23
+
24
+ export type AvatarWithLabelState = {
25
+ size?: ElementSize,
26
+ }
27
+
28
+ export type AvatarGroupState = {
29
+ size?: ElementSize,
30
+ count?: number,
31
+ }
32
+
33
+ export type AvatarStyle = ViewStyle
34
+
35
+ export type AvatarImageStyle = ImageStyle
36
+
37
+ export type AvatarTextStyle = TextStyle
38
+
39
+ export type AvatarIconStyle = {
40
+ size: number,
41
+ strokeWidth: number,
42
+ color: string,
43
+ }
44
+
45
+ export type AvatarStatusDotStyle = ViewStyle
46
+
47
+ export type AvatarWithStatusContainerStyle = ViewStyle
48
+
49
+ export type AvatarWithLabelContainerStyle = ViewStyle
50
+
51
+ export type AvatarWithLabelTextStyle = TextStyle
52
+
53
+ export type AvatarGroupContainerStyle = ViewStyle
54
+
55
+ export type AvatarGroupStackStyle = ViewStyle
56
+
57
+ export type AvatarGroupMoreStyle = TextStyle
58
+
59
+ export type AvatarTheme = {
60
+ avatar: StyleResolverFunction<AvatarState, AvatarStyle>,
61
+ image: StyleResolverFunction<AvatarState, AvatarImageStyle>,
62
+ text: StyleResolverFunction<AvatarState, AvatarTextStyle>,
63
+ icon: StyleResolverFunction<AvatarState, AvatarIconStyle>,
64
+ withStatus: {
65
+ container: StyleResolverFunction<AvatarWithStatusState, AvatarWithStatusContainerStyle>,
66
+ statusDot: StyleResolverFunction<AvatarWithStatusState, AvatarStatusDotStyle>,
67
+ },
68
+ withLabel: {
69
+ container: StyleResolverFunction<AvatarWithLabelState, AvatarWithLabelContainerStyle>,
70
+ text: StyleResolverFunction<AvatarWithLabelState, AvatarWithLabelTextStyle>,
71
+ },
72
+ group: {
73
+ container: StyleResolverFunction<AvatarGroupState, AvatarGroupContainerStyle>,
74
+ stack: StyleResolverFunction<AvatarGroupState, AvatarGroupStackStyle>,
75
+ more: StyleResolverFunction<AvatarGroupState, AvatarGroupMoreStyle>,
76
+ },
77
+ }
@@ -3,7 +3,7 @@ import type {
3
3
  ViewStyle
4
4
  } from 'react-native'
5
5
 
6
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
6
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
7
7
  import type {
8
8
  ButtonColoringStyle,
9
9
  ElementSize
@@ -3,7 +3,7 @@ import type {
3
3
  ViewStyle
4
4
  } from 'react-native'
5
5
 
6
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
6
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
7
7
 
8
8
  import type { Color } from '../color'
9
9
  import type {
@@ -3,7 +3,7 @@ import type {
3
3
  ViewStyle
4
4
  } from 'react-native'
5
5
 
6
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
6
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
7
7
  import type {
8
8
  ChipColoringStyle,
9
9
  ElementSize
@@ -1,4 +1,5 @@
1
1
  import type { Color } from '../color'
2
+ import type { AvatarTheme } from './avatar'
2
3
  import type { ButtonTheme } from './button'
3
4
  import type { ChatTheme } from './chat'
4
5
  import type { CheckboxTheme } from './checkbox'
@@ -39,4 +40,5 @@ export type HightideComponentThemes = {
39
40
  multiSelect: MultiSelectTheme,
40
41
  chat: ChatTheme,
41
42
  menu: MenuTheme,
43
+ avatar: AvatarTheme,
42
44
  }
@@ -1,6 +1,6 @@
1
1
  import type { ViewStyle } from 'react-native'
2
2
 
3
- import type { ColoringType } from '@helpwave/hightide-design/helpers'
3
+ import type { ColoringType } from '@helpwave/hightide-design/utils'
4
4
  import type {
5
5
  ButtonColoringStyle,
6
6
  ElementSize
@@ -1,3 +1,4 @@
1
+ export * from './avatar'
1
2
  export * from './button'
2
3
  export * from './chat'
3
4
  export * from './checkbox'
@@ -43,4 +43,16 @@ export type HightideLayout = {
43
43
  borderRadius: number,
44
44
  fontSize: number,
45
45
  }>,
46
+ avatar: Record<ElementSize, {
47
+ size: number,
48
+ padding: number,
49
+ fontSize: number,
50
+ statusDotSize: number,
51
+ statusDotBorderWidth: number,
52
+ }>,
53
+ avatarGroup: {
54
+ overlap: number,
55
+ maxShown: number,
56
+ gap: number,
57
+ },
46
58
  }
@@ -8,10 +8,19 @@ import type { HightideLayout } from './layout'
8
8
  import type { HightideTypography } from './typography'
9
9
 
10
10
  export type Theme = {
11
- colors: Record<string, unknown> & HightideColors,
12
- semantic: Record<string, unknown> & HightideSemanticColors,
13
- components: Record<string, unknown> & HightideComponentThemes,
14
- typography: Record<string, unknown> & HightideTypography,
15
- layout: Record<string, unknown> & HightideLayout,
16
- decoration: Record<string, unknown> & HightideDecoration,
11
+ colors: Record<string, unknown>,
12
+ semantic: Record<string, unknown>,
13
+ components: Record<string, unknown>,
14
+ typography: Record<string, unknown>,
15
+ layout: Record<string, unknown>,
16
+ decoration: Record<string, unknown>,
17
+ }
18
+
19
+ export type HightideTheme = Theme & {
20
+ colors: HightideColors & Theme['colors'],
21
+ semantic: HightideSemanticColors & Theme['semantic'],
22
+ components: HightideComponentThemes & Theme['components'],
23
+ typography: HightideTypography & Theme['typography'],
24
+ layout: HightideLayout & Theme['layout'],
25
+ decoration: HightideDecoration & Theme['decoration'],
17
26
  }
@@ -1,16 +0,0 @@
1
- import { Themes as designThemes } from '@helpwave/hightide-design/tokens'
2
-
3
- import { createTheme } from './createTheme'
4
-
5
- export const lightTheme = createTheme(designThemes.light)
6
-
7
- export const darkTheme = createTheme(designThemes.dark)
8
-
9
- export const themes = {
10
- light: lightTheme,
11
- dark: darkTheme,
12
- } as const
13
-
14
- export type HightideThemeModes = 'dark' | 'light'
15
-
16
- export type ThemeMode = string | HightideThemeModes