@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.
- package/package.json +4 -3
- package/src/components/chat/ChatAttachmentCard.tsx +4 -4
- package/src/components/chat/ChatConversationList.tsx +3 -3
- package/src/components/chat/ChatConversationRow.tsx +3 -3
- package/src/components/chat/ChatDateDivider.tsx +3 -3
- package/src/components/chat/ChatMessageBubble.tsx +3 -3
- package/src/components/chat/ChatMessageCard.tsx +4 -4
- package/src/components/chat/ChatMessageComposer.tsx +4 -4
- package/src/components/chat/ChatMessageList.tsx +3 -3
- package/src/components/chat/ChatQuickReplyChip.tsx +3 -3
- package/src/components/chat/ChatSystemLine.tsx +4 -4
- package/src/components/chat/ChatThreadHeader.tsx +3 -3
- package/src/components/menu/Menu.tsx +3 -3
- package/src/components/menu/MenuActionItem.tsx +3 -3
- package/src/components/menu/MenuItem.tsx +3 -3
- package/src/components/menu/MenuNavigationItem.tsx +3 -3
- package/src/components/user-interaction/Button.tsx +4 -4
- package/src/components/user-interaction/Checkbox.tsx +5 -5
- package/src/components/user-interaction/IconButton.tsx +5 -5
- package/src/components/user-interaction/Input.tsx +4 -4
- package/src/components/user-interaction/MultiSelect.tsx +6 -6
- package/src/components/user-interaction/Select.tsx +4 -4
- package/src/components/visualization-and-display/Avatar.tsx +286 -0
- package/src/components/visualization-and-display/Chip.tsx +4 -4
- package/src/components/visualization-and-display/Icon.tsx +1 -1
- package/src/components/visualization-and-display/index.ts +1 -0
- package/src/global-contexts/HightideProvider.tsx +5 -5
- package/src/global-contexts/hightide-config/HightideConfigUtils.ts +4 -4
- package/src/global-contexts/localization/LocalizationProvider.tsx +2 -2
- package/src/global-contexts/theme/ThemeContext.ts +3 -3
- package/src/global-contexts/theme/ThemeProvider.tsx +7 -7
- package/src/theme/resolvers/avatar.ts +231 -0
- package/src/theme/resolvers/button.ts +6 -6
- package/src/theme/resolvers/chat.ts +6 -6
- package/src/theme/resolvers/checkbox.ts +4 -4
- package/src/theme/resolvers/chip.ts +6 -6
- package/src/theme/resolvers/coloring.ts +4 -4
- package/src/theme/resolvers/iconButton.ts +6 -6
- package/src/theme/resolvers/index.ts +1 -0
- package/src/theme/resolvers/input.ts +4 -4
- package/src/theme/resolvers/menu.ts +4 -4
- package/src/theme/resolvers/multiSelect.ts +4 -4
- package/src/theme/resolvers/select.ts +4 -4
- package/src/theme/themes/{createTheme.ts → createHightideTheme.ts} +16 -14
- package/src/theme/themes/hightideThemes.ts +14 -0
- package/src/theme/themes/index.ts +2 -2
- package/src/theme/types/components/avatar.ts +77 -0
- package/src/theme/types/components/button.ts +2 -2
- package/src/theme/types/components/chat.ts +3 -3
- package/src/theme/types/components/checkbox.ts +2 -2
- package/src/theme/types/components/chip.ts +2 -2
- package/src/theme/types/components/hightide.ts +12 -10
- package/src/theme/types/components/iconButton.ts +3 -3
- package/src/theme/types/components/index.ts +1 -0
- package/src/theme/types/components/input.ts +2 -2
- package/src/theme/types/components/menu.ts +2 -2
- package/src/theme/types/components/multiSelect.ts +3 -3
- package/src/theme/types/components/select.ts +2 -2
- package/src/theme/types/layout.ts +12 -0
- package/src/theme/types/theme.ts +20 -11
- 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/
|
|
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 '
|
|
22
|
+
import { useTheme } from '../../global-contexts/theme/ThemeContext'
|
|
23
23
|
import type {
|
|
24
24
|
ChipState,
|
|
25
25
|
ChipStyle,
|
|
26
26
|
ChipTextStyle
|
|
27
|
-
} from '
|
|
28
|
-
import type { StyleOverwrite } from '
|
|
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 '
|
|
5
|
+
import { useTheme } from '../../global-contexts/theme/ThemeContext'
|
|
6
6
|
|
|
7
7
|
export type IconProps = {
|
|
8
8
|
icon: LucideIcon,
|
|
@@ -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 '
|
|
10
|
+
import { HightideContext } from './HightideContext'
|
|
11
11
|
import {
|
|
12
12
|
LocalizationProvider,
|
|
13
13
|
type LocalizationProviderProps
|
|
14
|
-
} from '
|
|
15
|
-
import { useTheme } from '
|
|
14
|
+
} from './localization/LocalizationProvider'
|
|
15
|
+
import { useTheme } from './theme/ThemeContext'
|
|
16
16
|
import {
|
|
17
17
|
ThemeProvider,
|
|
18
18
|
type ThemeProviderProps
|
|
19
|
-
} from '
|
|
19
|
+
} from './theme/ThemeProvider'
|
|
20
20
|
import {
|
|
21
21
|
TranslationProvider,
|
|
22
22
|
type TranslationProviderProps
|
|
23
|
-
} from '
|
|
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 '
|
|
4
|
-
import { themes } from '
|
|
5
|
-
import type {
|
|
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<
|
|
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 '
|
|
9
|
-
import { useNativeKeyValueStore } from '
|
|
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 '
|
|
10
|
-
import type {
|
|
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<
|
|
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 '
|
|
11
|
+
import { HightideConfigUtils } from '../hightide-config/HightideConfigUtils'
|
|
12
12
|
import {
|
|
13
13
|
ThemeContext,
|
|
14
14
|
useTheme,
|
|
15
15
|
type ThemeContextValue
|
|
16
|
-
} from '
|
|
16
|
+
} from './ThemeContext'
|
|
17
17
|
import {
|
|
18
18
|
useCreateThemeConfig,
|
|
19
19
|
type SystemTheme
|
|
20
|
-
} from '
|
|
21
|
-
import { useNativeKeyValueStore } from '
|
|
22
|
-
import type { ThemeMode } from '
|
|
23
|
-
import type {
|
|
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<
|
|
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
|
+
}
|