@helpwave/hightide-native 0.0.1

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 (65) hide show
  1. package/LICENSE +208 -0
  2. package/package.json +93 -0
  3. package/src/components/Button/Button.tsx +101 -0
  4. package/src/components/Button/index.ts +1 -0
  5. package/src/components/Checkbox/Checkbox.tsx +103 -0
  6. package/src/components/Checkbox/index.ts +1 -0
  7. package/src/components/Chip/Chip.tsx +93 -0
  8. package/src/components/Chip/index.ts +1 -0
  9. package/src/components/IconButton/IconButton.tsx +89 -0
  10. package/src/components/IconButton/index.ts +1 -0
  11. package/src/components/Input/Input.tsx +114 -0
  12. package/src/components/Input/index.ts +1 -0
  13. package/src/components/MultiSelect/MultiSelect.tsx +153 -0
  14. package/src/components/MultiSelect/index.ts +1 -0
  15. package/src/components/Select/Select.tsx +136 -0
  16. package/src/components/Select/index.ts +1 -0
  17. package/src/components/index.ts +7 -0
  18. package/src/global-contexts/HightideContext.ts +18 -0
  19. package/src/global-contexts/HightideProvider.tsx +57 -0
  20. package/src/global-contexts/hightide-config/HightideConfigUtils.ts +25 -0
  21. package/src/global-contexts/hightide-config/forward-exports.ts +1 -0
  22. package/src/global-contexts/hightide-config/index.ts +2 -0
  23. package/src/global-contexts/index.ts +6 -0
  24. package/src/global-contexts/localization/LocalizationProvider.tsx +31 -0
  25. package/src/global-contexts/localization/forward-exports.ts +8 -0
  26. package/src/global-contexts/localization/index.ts +2 -0
  27. package/src/global-contexts/theme/ThemeContext.ts +23 -0
  28. package/src/global-contexts/theme/ThemeProvider.tsx +101 -0
  29. package/src/global-contexts/theme/forward-exports.ts +7 -0
  30. package/src/global-contexts/theme/index.ts +3 -0
  31. package/src/global-contexts/translation/forward-exports.ts +11 -0
  32. package/src/global-contexts/translation/index.ts +1 -0
  33. package/src/hooks/index.ts +3 -0
  34. package/src/hooks/useMultiSelect.ts +232 -0
  35. package/src/hooks/useNativeKeyValueStore.ts +59 -0
  36. package/src/hooks/useSelect.ts +198 -0
  37. package/src/icons/Icon.tsx +35 -0
  38. package/src/icons/index.ts +1 -0
  39. package/src/storybook/helper.tsx +36 -0
  40. package/src/theme/index.ts +3 -0
  41. package/src/theme/resolvers/button.ts +74 -0
  42. package/src/theme/resolvers/checkbox.ts +86 -0
  43. package/src/theme/resolvers/chip.ts +64 -0
  44. package/src/theme/resolvers/coloring.ts +90 -0
  45. package/src/theme/resolvers/iconButton.ts +62 -0
  46. package/src/theme/resolvers/index.ts +8 -0
  47. package/src/theme/resolvers/input.ts +48 -0
  48. package/src/theme/resolvers/multiSelect.ts +95 -0
  49. package/src/theme/resolvers/select.ts +77 -0
  50. package/src/theme/themes/createTheme.ts +26 -0
  51. package/src/theme/themes/index.ts +2 -0
  52. package/src/theme/themes/nativeThemes.ts +15 -0
  53. package/src/theme/types/button.ts +41 -0
  54. package/src/theme/types/checkbox.ts +36 -0
  55. package/src/theme/types/chip.ts +41 -0
  56. package/src/theme/types/components.ts +17 -0
  57. package/src/theme/types/iconButton.ts +34 -0
  58. package/src/theme/types/index.ts +10 -0
  59. package/src/theme/types/input.ts +25 -0
  60. package/src/theme/types/multiSelect.ts +65 -0
  61. package/src/theme/types/resolver.ts +10 -0
  62. package/src/theme/types/select.ts +77 -0
  63. package/src/theme/types/theme.ts +13 -0
  64. package/src/types/formField.ts +12 -0
  65. package/src/types/index.ts +1 -0
@@ -0,0 +1,89 @@
1
+ import {
2
+ type ButtonColoringStyle,
3
+ type ColoringType,
4
+ type ElementSize
5
+ } from '@helpwave/hightide-design'
6
+ import type { LucideIcon } from 'lucide-react-native'
7
+ import { forwardRef, type ReactNode } from 'react'
8
+ import { Pressable, type PressableProps, type StyleProp, type ViewStyle } from 'react-native'
9
+ import { Icon } from '../../icons/Icon'
10
+ import { useTheme } from '../../global-contexts/theme'
11
+ import type { IconButtonState, IconButtonStyle } from '../../theme'
12
+
13
+ export type IconButtonSize = ElementSize
14
+
15
+ export type IconButtonProps = Omit<PressableProps, 'children' | 'style'> & {
16
+ size?: IconButtonSize,
17
+ color?: ColoringType,
18
+ coloringStyle?: ButtonColoringStyle,
19
+ icon?: LucideIcon,
20
+ iconSize?: ElementSize,
21
+ children?: ReactNode,
22
+ accessibilityLabel: string,
23
+ style?: StyleProp<ViewStyle>,
24
+ buttonStyle?: StyleProp<ViewStyle> | ((style: IconButtonStyle) => StyleProp<ViewStyle>),
25
+ }
26
+
27
+ type PressableInteraction = {
28
+ pressed: boolean,
29
+ hovered?: boolean,
30
+ focused?: boolean,
31
+ }
32
+
33
+ export const IconButton = forwardRef<React.ComponentRef<typeof Pressable>, IconButtonProps>(function IconButton({
34
+ children,
35
+ icon,
36
+ iconSize,
37
+ size = 'md',
38
+ color = 'neutral',
39
+ coloringStyle = 'solid',
40
+ disabled,
41
+ accessibilityLabel,
42
+ style,
43
+ buttonStyle,
44
+ ...props
45
+ }, ref) {
46
+ const { theme } = useTheme()
47
+
48
+ const resolveStyles = (interaction: PressableInteraction) => {
49
+ const state: IconButtonState = {
50
+ size,
51
+ color,
52
+ coloringStyle,
53
+ isDisabled: !!disabled,
54
+ isPressed: interaction.pressed,
55
+ isHovered: !!interaction.hovered,
56
+ isFocused: !!interaction.focused,
57
+ }
58
+
59
+ const resolvedButton = theme.components.iconButton.button(state)
60
+ const resolvedIcon = theme.components.iconButton.icon(state)
61
+
62
+ return {
63
+ button: typeof buttonStyle === 'function' ? buttonStyle(resolvedButton) : [resolvedButton, buttonStyle],
64
+ icon: resolvedIcon,
65
+ }
66
+ }
67
+
68
+ return (
69
+ <Pressable
70
+ {...props}
71
+ ref={ref}
72
+ disabled={disabled}
73
+ accessibilityRole="button"
74
+ accessibilityLabel={accessibilityLabel}
75
+ style={(pressableState) => {
76
+ const resolved = resolveStyles(pressableState as PressableInteraction)
77
+ return [resolved.button, style]
78
+ }}
79
+ >
80
+ {(pressableState) => {
81
+ const resolved = resolveStyles(pressableState as PressableInteraction)
82
+ if (icon) {
83
+ return <Icon icon={icon} size={iconSize ?? size} color={resolved.icon.color} />
84
+ }
85
+ return children
86
+ }}
87
+ </Pressable>
88
+ )
89
+ })
@@ -0,0 +1 @@
1
+ export * from './IconButton'
@@ -0,0 +1,114 @@
1
+ import {
2
+ useControlledState,
3
+ useDelay,
4
+ type UseDelayOptionsResolved
5
+ } from '@helpwave/hightide-utils/hooks'
6
+ import { forwardRef } from 'react'
7
+ import { TextInput, type StyleProp, type TextInputProps, type TextStyle } from 'react-native'
8
+ import { useTheme } from '../../global-contexts/theme'
9
+ import type { InputState, InputStyle } from '../../theme'
10
+ import type { FormFieldDataHandling, FormFieldInteractionStates } from '../../types/formField'
11
+
12
+ export type EditCompleteOptionsResolved = {
13
+ onBlur: boolean,
14
+ afterDelay: boolean,
15
+ allowEnterComplete?: boolean,
16
+ } & Omit<UseDelayOptionsResolved, 'disabled'>
17
+
18
+ export type EditCompleteOptions = Partial<EditCompleteOptionsResolved>
19
+
20
+ const defaultEditCompleteOptions: EditCompleteOptionsResolved = {
21
+ allowEnterComplete: false,
22
+ onBlur: true,
23
+ afterDelay: false,
24
+ delay: 2500,
25
+ }
26
+
27
+ export type InputProps = Omit<TextInputProps, 'value' | 'style'>
28
+ & Partial<FormFieldDataHandling<string>>
29
+ & Partial<FormFieldInteractionStates>
30
+ & {
31
+ editCompleteOptions?: EditCompleteOptions,
32
+ initialValue?: string,
33
+ style?: StyleProp<TextStyle>,
34
+ inputStyle?: StyleProp<TextStyle> | ((style: InputStyle) => StyleProp<TextStyle>),
35
+ }
36
+
37
+ export const Input = forwardRef<TextInput, InputProps>(function Input({
38
+ value: controlledValue,
39
+ initialValue,
40
+ invalid = false,
41
+ disabled = false,
42
+ readOnly = false,
43
+ required = false,
44
+ onValueChange,
45
+ onEditComplete,
46
+ editCompleteOptions,
47
+ style,
48
+ inputStyle,
49
+ ...props
50
+ }, ref) {
51
+ const { theme } = useTheme()
52
+
53
+ const [value, setValue] = useControlledState({
54
+ value: controlledValue,
55
+ onValueChange,
56
+ defaultValue: initialValue,
57
+ })
58
+
59
+ const {
60
+ onBlur: allowEditCompleteOnBlur,
61
+ afterDelay,
62
+ delay,
63
+ allowEnterComplete,
64
+ } = { ...defaultEditCompleteOptions, ...editCompleteOptions }
65
+
66
+ const { restartTimer, clearTimer } = useDelay({
67
+ delay,
68
+ disabled: !afterDelay || disabled || readOnly,
69
+ })
70
+
71
+ const state: InputState = {
72
+ isDisabled: disabled,
73
+ isInvalid: invalid,
74
+ isReadOnly: readOnly,
75
+ }
76
+
77
+ const resolvedInput = theme.components.input.input(state)
78
+ const appliedInputStyle = typeof inputStyle === 'function'
79
+ ? inputStyle(resolvedInput)
80
+ : [resolvedInput, inputStyle]
81
+
82
+ return (
83
+ <TextInput
84
+ {...props}
85
+ ref={ref}
86
+ value={value}
87
+ editable={!disabled && !readOnly}
88
+ onChangeText={(nextValue) => {
89
+ props.onChangeText?.(nextValue)
90
+ restartTimer(() => {
91
+ onEditComplete?.(nextValue)
92
+ })
93
+ setValue(nextValue)
94
+ }}
95
+ onBlur={(event) => {
96
+ props.onBlur?.(event)
97
+ if (allowEditCompleteOnBlur) {
98
+ onEditComplete?.(value ?? '')
99
+ clearTimer()
100
+ }
101
+ }}
102
+ onSubmitEditing={(event) => {
103
+ props.onSubmitEditing?.(event)
104
+ if (allowEnterComplete) {
105
+ onEditComplete?.(value ?? '')
106
+ clearTimer()
107
+ }
108
+ }}
109
+ placeholderTextColor={theme.components.input.placeholderColor(state)}
110
+ style={[appliedInputStyle, style]}
111
+ accessibilityState={{ disabled, selected: required }}
112
+ />
113
+ )
114
+ })
@@ -0,0 +1 @@
1
+ export * from './Input'
@@ -0,0 +1,153 @@
1
+ import { Check } from 'lucide-react-native'
2
+ import { useMemo } from 'react'
3
+ import {
4
+ FlatList,
5
+ Modal,
6
+ Pressable,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ type StyleProp,
11
+ type ViewStyle
12
+ } from 'react-native'
13
+ import { Chip } from '../Chip/Chip'
14
+ import { Icon } from '../../icons/Icon'
15
+ import { useMultiSelect, type UseMultiSelectOption } from '../../hooks/useMultiSelect'
16
+ import { useTheme } from '../../global-contexts/theme'
17
+ import type { MultiSelectState } from '../../theme'
18
+ import type { FormFieldDataHandling, FormFieldInteractionStates } from '../../types/formField'
19
+
20
+ export type MultiSelectOption = UseMultiSelectOption
21
+
22
+ export type MultiSelectProps = Partial<FormFieldDataHandling<string[]>>
23
+ & Partial<FormFieldInteractionStates>
24
+ & {
25
+ options: ReadonlyArray<MultiSelectOption>,
26
+ value?: string[],
27
+ initialValue?: string[],
28
+ placeholder?: string,
29
+ showSearch?: boolean,
30
+ style?: StyleProp<ViewStyle>,
31
+ }
32
+
33
+ export const MultiSelect = ({
34
+ options,
35
+ value: controlledValue,
36
+ initialValue = [],
37
+ placeholder = 'Select…',
38
+ showSearch = true,
39
+ disabled = false,
40
+ readOnly = false,
41
+ invalid = false,
42
+ onValueChange,
43
+ onEditComplete,
44
+ style,
45
+ }: MultiSelectProps) => {
46
+ const { theme } = useTheme()
47
+ const interactive = !disabled && !readOnly
48
+
49
+ const multiSelect = useMultiSelect({
50
+ options,
51
+ value: controlledValue,
52
+ initialValue,
53
+ onValueChange,
54
+ onEditComplete,
55
+ })
56
+
57
+ const selectedLabels = useMemo(() => {
58
+ return options
59
+ .filter((option) => multiSelect.isSelected(option.id))
60
+ .map((option) => option.label ?? option.id)
61
+ }, [multiSelect, options])
62
+
63
+ const state: MultiSelectState = {
64
+ isDisabled: disabled,
65
+ isReadOnly: readOnly,
66
+ isInvalid: invalid,
67
+ isOpen: multiSelect.isOpen,
68
+ hasSelections: selectedLabels.length > 0,
69
+ hasValue: selectedLabels.length > 0,
70
+ }
71
+
72
+ const multiSelectTheme = theme.components.multiSelect
73
+
74
+ return (
75
+ <View style={style}>
76
+ <Pressable
77
+ disabled={!interactive}
78
+ onPress={() => multiSelect.toggleOpen()}
79
+ style={multiSelectTheme.trigger(state)}
80
+ >
81
+ {selectedLabels.length > 0
82
+ ? (
83
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
84
+ {selectedLabels.map((label) => (
85
+ <Chip key={label} size="sm" color="primary" coloringStyle="tonal">
86
+ {label}
87
+ </Chip>
88
+ ))}
89
+ </View>
90
+ )
91
+ : <Text style={multiSelectTheme.triggerText(state)}>{placeholder}</Text>}
92
+ </Pressable>
93
+
94
+ <Modal
95
+ visible={multiSelect.isOpen}
96
+ transparent
97
+ animationType="fade"
98
+ onRequestClose={() => multiSelect.setIsOpen(false)}
99
+ >
100
+ <Pressable
101
+ style={multiSelectTheme.overlay(state)}
102
+ onPress={() => multiSelect.setIsOpen(false)}
103
+ >
104
+ <Pressable
105
+ style={multiSelectTheme.menu(state)}
106
+ onPress={(event) => event.stopPropagation()}
107
+ >
108
+ {showSearch && (
109
+ <TextInput
110
+ value={multiSelect.searchQuery}
111
+ onChangeText={multiSelect.setSearchQuery}
112
+ placeholder="Search…"
113
+ placeholderTextColor={multiSelectTheme.searchPlaceholderColor(state)}
114
+ style={multiSelectTheme.search(state)}
115
+ />
116
+ )}
117
+ <FlatList
118
+ data={options.filter((option) => multiSelect.visibleOptionIds.includes(option.id))}
119
+ keyExtractor={(option) => option.id}
120
+ renderItem={({ item }) => {
121
+ const selected = multiSelect.isSelected(item.id)
122
+ const isHighlighted = multiSelect.highlightedId === item.id
123
+ const optionState = {
124
+ isSelected: selected,
125
+ isHighlighted,
126
+ isDisabled: item.disabled,
127
+ }
128
+ const checkboxIcon = multiSelectTheme.checkboxIcon(optionState)
129
+
130
+ return (
131
+ <Pressable
132
+ disabled={item.disabled}
133
+ onPress={() => multiSelect.toggleSelection(item.id)}
134
+ style={multiSelectTheme.option(optionState)}
135
+ >
136
+ <View style={multiSelectTheme.checkbox(optionState)}>
137
+ {checkboxIcon.visible && (
138
+ <Icon icon={Check} size="sm" color={checkboxIcon.color} />
139
+ )}
140
+ </View>
141
+ <Text style={multiSelectTheme.optionText(optionState)}>
142
+ {item.label}
143
+ </Text>
144
+ </Pressable>
145
+ )
146
+ }}
147
+ />
148
+ </Pressable>
149
+ </Pressable>
150
+ </Modal>
151
+ </View>
152
+ )
153
+ }
@@ -0,0 +1 @@
1
+ export * from './MultiSelect'
@@ -0,0 +1,136 @@
1
+ import { useMemo } from 'react'
2
+ import {
3
+ FlatList,
4
+ Modal,
5
+ Pressable,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ type StyleProp,
10
+ type ViewStyle
11
+ } from 'react-native'
12
+ import { useSelect, type UseSelectOption } from '../../hooks/useSelect'
13
+ import { useTheme } from '../../global-contexts/theme'
14
+ import type { SelectState } from '../../theme'
15
+ import type { FormFieldDataHandling, FormFieldInteractionStates } from '../../types/formField'
16
+
17
+ export type SelectOption<T extends string = string> = UseSelectOption & {
18
+ value?: T,
19
+ }
20
+
21
+ export type SelectProps = Partial<FormFieldDataHandling<string>>
22
+ & Partial<FormFieldInteractionStates>
23
+ & {
24
+ options: ReadonlyArray<SelectOption>,
25
+ value?: string | null,
26
+ initialValue?: string | null,
27
+ placeholder?: string,
28
+ showSearch?: boolean,
29
+ style?: StyleProp<ViewStyle>,
30
+ }
31
+
32
+ export const Select = ({
33
+ options,
34
+ value: controlledValue,
35
+ initialValue = null,
36
+ placeholder = 'Select…',
37
+ showSearch = true,
38
+ disabled = false,
39
+ readOnly = false,
40
+ invalid = false,
41
+ onValueChange,
42
+ onEditComplete,
43
+ style,
44
+ }: SelectProps) => {
45
+ const { theme } = useTheme()
46
+ const interactive = !disabled && !readOnly
47
+
48
+ const select = useSelect({
49
+ options,
50
+ value: controlledValue,
51
+ initialValue,
52
+ onValueChange,
53
+ onEditComplete,
54
+ })
55
+
56
+ const selectedLabel = useMemo(() => {
57
+ const selected = options.find((option) => option.id === select.value)
58
+ return selected?.label ?? placeholder
59
+ }, [options, placeholder, select.value])
60
+
61
+ const state: SelectState = {
62
+ isDisabled: disabled,
63
+ isReadOnly: readOnly,
64
+ isInvalid: invalid,
65
+ isOpen: select.isOpen,
66
+ hasValue: !!select.value,
67
+ }
68
+
69
+ const selectTheme = theme.components.select
70
+
71
+ return (
72
+ <View style={style}>
73
+ <Pressable
74
+ disabled={!interactive}
75
+ onPress={() => select.toggleOpen()}
76
+ style={selectTheme.trigger(state)}
77
+ >
78
+ <Text style={selectTheme.triggerText(state)}>
79
+ {selectedLabel}
80
+ </Text>
81
+ </Pressable>
82
+
83
+ <Modal
84
+ visible={select.isOpen}
85
+ transparent
86
+ animationType="fade"
87
+ onRequestClose={() => select.setIsOpen(false)}
88
+ >
89
+ <Pressable
90
+ style={selectTheme.overlay(state)}
91
+ onPress={() => select.setIsOpen(false)}
92
+ >
93
+ <Pressable
94
+ style={selectTheme.menu(state)}
95
+ onPress={(event) => event.stopPropagation()}
96
+ >
97
+ {showSearch && (
98
+ <TextInput
99
+ value={select.searchQuery}
100
+ onChangeText={select.setSearchQuery}
101
+ placeholder="Search…"
102
+ placeholderTextColor={selectTheme.searchPlaceholderColor(state)}
103
+ style={selectTheme.search(state)}
104
+ />
105
+ )}
106
+ <FlatList
107
+ data={options.filter((option) => select.visibleOptionIds.includes(option.id))}
108
+ keyExtractor={(option) => option.id}
109
+ renderItem={({ item }) => {
110
+ const isSelected = select.value === item.id
111
+ const isHighlighted = select.highlightedValue === item.id
112
+ const optionState = {
113
+ isSelected,
114
+ isHighlighted,
115
+ isDisabled: item.disabled,
116
+ }
117
+
118
+ return (
119
+ <Pressable
120
+ disabled={item.disabled}
121
+ onPress={() => select.selectValue(item.id)}
122
+ style={selectTheme.option(optionState)}
123
+ >
124
+ <Text style={selectTheme.optionText(optionState)}>
125
+ {item.label}
126
+ </Text>
127
+ </Pressable>
128
+ )
129
+ }}
130
+ />
131
+ </Pressable>
132
+ </Pressable>
133
+ </Modal>
134
+ </View>
135
+ )
136
+ }
@@ -0,0 +1 @@
1
+ export * from './Select'
@@ -0,0 +1,7 @@
1
+ export * from './Button'
2
+ export * from './Checkbox'
3
+ export * from './Chip'
4
+ export * from './IconButton'
5
+ export * from './Input'
6
+ export * from './MultiSelect'
7
+ export * from './Select'
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export type HightideContextValue = {
4
+ isLocalizationInitialized: boolean,
5
+ isThemeInitialized: boolean,
6
+ }
7
+
8
+ export const HightideContext = createContext<HightideContextValue | null>(null)
9
+
10
+ export const useHightide = (): HightideContextValue => {
11
+ const context = useContext(HightideContext)
12
+ if (!context) {
13
+ throw new Error(
14
+ 'useHightide must be used within HightideContext. Try adding a HightideProvider around your app.'
15
+ )
16
+ }
17
+ return context
18
+ }
@@ -0,0 +1,57 @@
1
+ import { useMemo, type PropsWithChildren } from 'react'
2
+ import { ArrayUtil } from '@helpwave/hightide-utils/utils'
3
+ import { hightideTranslation } from '@helpwave/hightide-utils/i18n'
4
+ import { useLocalization } from '@helpwave/hightide-utils/context/localization'
5
+ import { HightideContext } from './HightideContext'
6
+ import type { LocalizationProviderProps } from './localization'
7
+ import { LocalizationProvider } from './localization'
8
+ import type { ThemeProviderProps } from './theme'
9
+ import { ThemeProvider, useTheme } from './theme'
10
+ import type { TranslationProviderProps } from './translation'
11
+ import { TranslationProvider } from './translation'
12
+
13
+ export type HightideProviderProps = PropsWithChildren & {
14
+ theme?: Omit<ThemeProviderProps, 'children'>,
15
+ locale?: Omit<LocalizationProviderProps, 'children'>,
16
+ translation?: Omit<TranslationProviderProps, 'children' | 'locale'>,
17
+ }
18
+
19
+ const HightideContextBridge = ({ children }: PropsWithChildren) => {
20
+ const { isInitialized: isLocalizationInitialized } = useLocalization()
21
+ const { isInitialized: isThemeInitialized } = useTheme()
22
+
23
+ const value = useMemo(() => ({
24
+ isLocalizationInitialized,
25
+ isThemeInitialized,
26
+ }), [isLocalizationInitialized, isThemeInitialized])
27
+
28
+ return (
29
+ <HightideContext.Provider value={value}>
30
+ {children}
31
+ </HightideContext.Provider>
32
+ )
33
+ }
34
+
35
+ export const HightideProvider = ({
36
+ children,
37
+ theme,
38
+ locale,
39
+ translation,
40
+ }: HightideProviderProps) => {
41
+ const resolvedTranslations = useMemo(() => [
42
+ ...ArrayUtil.resolveSingleOrArray(translation?.translation ?? []),
43
+ hightideTranslation,
44
+ ], [translation?.translation])
45
+
46
+ return (
47
+ <LocalizationProvider {...locale}>
48
+ <TranslationProvider {...translation} translation={resolvedTranslations}>
49
+ <ThemeProvider {...theme}>
50
+ <HightideContextBridge>
51
+ {children}
52
+ </HightideContextBridge>
53
+ </ThemeProvider>
54
+ </TranslationProvider>
55
+ </LocalizationProvider>
56
+ )
57
+ }
@@ -0,0 +1,25 @@
1
+ import type { LocaleInformation } from '../localization/forward-exports'
2
+ import type { ThemeInformation } from '../theme/forward-exports'
3
+
4
+ export const HightideConfigUtils = {
5
+ defaultSupportedLocales: [
6
+ { locale: 'de-DE', localName: 'Deutsch' },
7
+ { locale: 'en-US', localName: 'English (US)' },
8
+ ] as const satisfies readonly LocaleInformation[],
9
+ defaultSupportedThemes: [
10
+ {
11
+ theme: 'light',
12
+ nameTranslations: {
13
+ 'de-DE': 'Hell',
14
+ 'en-US': 'Light',
15
+ },
16
+ },
17
+ {
18
+ theme: 'dark',
19
+ nameTranslations: {
20
+ 'de-DE': 'Dunkel',
21
+ 'en-US': 'Dark',
22
+ },
23
+ },
24
+ ] as const satisfies readonly ThemeInformation[],
25
+ }
@@ -0,0 +1,2 @@
1
+ export * from './HightideConfigUtils'
2
+ export * from './forward-exports'
@@ -0,0 +1,6 @@
1
+ export * from './HightideContext'
2
+ export * from './HightideProvider'
3
+ export * from './hightide-config'
4
+ export * from './localization'
5
+ export * from './theme'
6
+ export * from './translation'
@@ -0,0 +1,31 @@
1
+ import type { PropsWithChildren } from 'react'
2
+ import {
3
+ LocalizationProvider as LocalizationProviderBase,
4
+ type LocalizationProviderProps as LocalizationProviderPropsBase
5
+ } from '@helpwave/hightide-utils/context/localization'
6
+ import { useNativeKeyValueStore } from '../../hooks/useNativeKeyValueStore'
7
+ import { HightideConfigUtils } from '../hightide-config/HightideConfigUtils'
8
+
9
+ export type LocalizationProviderProps = PropsWithChildren
10
+ & Omit<LocalizationProviderPropsBase, 'store' | 'fallbackLocale' | 'supportedLocales'>
11
+ & Partial<Pick<LocalizationProviderPropsBase, 'fallbackLocale' | 'supportedLocales'>>
12
+
13
+ export const LocalizationProvider = ({
14
+ children,
15
+ fallbackLocale = 'de-DE',
16
+ supportedLocales = HightideConfigUtils.defaultSupportedLocales,
17
+ ...rest
18
+ }: LocalizationProviderProps) => {
19
+ const store = useNativeKeyValueStore()
20
+
21
+ return (
22
+ <LocalizationProviderBase
23
+ store={store}
24
+ fallbackLocale={fallbackLocale}
25
+ supportedLocales={supportedLocales}
26
+ {...rest}
27
+ >
28
+ {children}
29
+ </LocalizationProviderBase>
30
+ )
31
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ LocalizationContext,
3
+ useLocalization,
4
+ useTimeZone,
5
+ useDateTimeFormat,
6
+ type LocalizationContextValue,
7
+ type LocaleInformation,
8
+ } from '@helpwave/hightide-utils/context/localization'
@@ -0,0 +1,2 @@
1
+ export * from './LocalizationProvider'
2
+ export * from './forward-exports'