@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.
- package/LICENSE +208 -0
- package/package.json +93 -0
- package/src/components/Button/Button.tsx +101 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +103 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/Chip/Chip.tsx +93 -0
- package/src/components/Chip/index.ts +1 -0
- package/src/components/IconButton/IconButton.tsx +89 -0
- package/src/components/IconButton/index.ts +1 -0
- package/src/components/Input/Input.tsx +114 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/MultiSelect/MultiSelect.tsx +153 -0
- package/src/components/MultiSelect/index.ts +1 -0
- package/src/components/Select/Select.tsx +136 -0
- package/src/components/Select/index.ts +1 -0
- package/src/components/index.ts +7 -0
- package/src/global-contexts/HightideContext.ts +18 -0
- package/src/global-contexts/HightideProvider.tsx +57 -0
- package/src/global-contexts/hightide-config/HightideConfigUtils.ts +25 -0
- package/src/global-contexts/hightide-config/forward-exports.ts +1 -0
- package/src/global-contexts/hightide-config/index.ts +2 -0
- package/src/global-contexts/index.ts +6 -0
- package/src/global-contexts/localization/LocalizationProvider.tsx +31 -0
- package/src/global-contexts/localization/forward-exports.ts +8 -0
- package/src/global-contexts/localization/index.ts +2 -0
- package/src/global-contexts/theme/ThemeContext.ts +23 -0
- package/src/global-contexts/theme/ThemeProvider.tsx +101 -0
- package/src/global-contexts/theme/forward-exports.ts +7 -0
- package/src/global-contexts/theme/index.ts +3 -0
- package/src/global-contexts/translation/forward-exports.ts +11 -0
- package/src/global-contexts/translation/index.ts +1 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useMultiSelect.ts +232 -0
- package/src/hooks/useNativeKeyValueStore.ts +59 -0
- package/src/hooks/useSelect.ts +198 -0
- package/src/icons/Icon.tsx +35 -0
- package/src/icons/index.ts +1 -0
- package/src/storybook/helper.tsx +36 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/resolvers/button.ts +74 -0
- package/src/theme/resolvers/checkbox.ts +86 -0
- package/src/theme/resolvers/chip.ts +64 -0
- package/src/theme/resolvers/coloring.ts +90 -0
- package/src/theme/resolvers/iconButton.ts +62 -0
- package/src/theme/resolvers/index.ts +8 -0
- package/src/theme/resolvers/input.ts +48 -0
- package/src/theme/resolvers/multiSelect.ts +95 -0
- package/src/theme/resolvers/select.ts +77 -0
- package/src/theme/themes/createTheme.ts +26 -0
- package/src/theme/themes/index.ts +2 -0
- package/src/theme/themes/nativeThemes.ts +15 -0
- package/src/theme/types/button.ts +41 -0
- package/src/theme/types/checkbox.ts +36 -0
- package/src/theme/types/chip.ts +41 -0
- package/src/theme/types/components.ts +17 -0
- package/src/theme/types/iconButton.ts +34 -0
- package/src/theme/types/index.ts +10 -0
- package/src/theme/types/input.ts +25 -0
- package/src/theme/types/multiSelect.ts +65 -0
- package/src/theme/types/resolver.ts +10 -0
- package/src/theme/types/select.ts +77 -0
- package/src/theme/types/theme.ts +13 -0
- package/src/types/formField.ts +12 -0
- 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,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 @@
|
|
|
1
|
+
export {}
|
|
@@ -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
|
+
}
|