@helpwave/hightide-native 0.0.7 → 0.0.8

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 CHANGED
@@ -10,7 +10,7 @@
10
10
  "access": "public"
11
11
  },
12
12
  "license": "AGPL-3.0-only",
13
- "version": "0.0.7",
13
+ "version": "0.0.8",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "src"
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "lucide-react-native": "0.561.0",
46
- "@helpwave/hightide-design": "0.0.5",
46
+ "@helpwave/hightide-design": "0.0.6",
47
47
  "@helpwave/hightide-utils": "0.0.5"
48
48
  },
49
49
  "peerDependencies": {
@@ -0,0 +1,177 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef
6
+ } from 'react'
7
+ import {
8
+ Animated,
9
+ Pressable,
10
+ View,
11
+ type PressableProps,
12
+ type StyleProp,
13
+ type ViewStyle
14
+ } from 'react-native'
15
+
16
+ import {
17
+ useControlledState,
18
+ useEventCallbackStabilizer
19
+ } from '@helpwave/hightide-utils/hooks'
20
+
21
+ import { useTheme } from '../../global-contexts/theme/ThemeContext'
22
+ import type { SwitchState } from '../../theme/types/components/switch'
23
+ import type { StyleOverwrite } from '../../theme/types/resolver'
24
+ import type { Color } from '../../theme/types/color'
25
+ import type {
26
+ FormFieldDataHandling,
27
+ FormFieldInteractionStates
28
+ } from '../../types/formField'
29
+
30
+ const TRACK_WIDTH = 48
31
+ const TRACK_HEIGHT = 28
32
+ const TRACK_BORDER_WIDTH = 2
33
+ const TRACK_PADDING = 4
34
+ const THUMB_SIZE_INACTIVE = 12
35
+ const THUMB_SIZE_ACTIVE = 16
36
+ const THUMB_OFFSET_INACTIVE = 2
37
+ const ANIMATION_DURATION_MS = 250
38
+
39
+ const TRACK_INNER_WIDTH = TRACK_WIDTH - (TRACK_BORDER_WIDTH * 2) - (TRACK_PADDING * 2)
40
+ const TRACK_INNER_HEIGHT = TRACK_HEIGHT - (TRACK_BORDER_WIDTH * 2) - (TRACK_PADDING * 2)
41
+ const THUMB_OFFSET_ACTIVE = TRACK_INNER_WIDTH - THUMB_SIZE_ACTIVE
42
+ const THUMB_TOP_INACTIVE = (TRACK_INNER_HEIGHT - THUMB_SIZE_INACTIVE) / 2
43
+ const THUMB_TOP_ACTIVE = (TRACK_INNER_HEIGHT - THUMB_SIZE_ACTIVE) / 2
44
+
45
+ export type SwitchProps = Omit<PressableProps, 'children' | 'style' | 'disabled'>
46
+ & Partial<FormFieldInteractionStates>
47
+ & Partial<FormFieldDataHandling<boolean>>
48
+ & {
49
+ initialValue?: boolean,
50
+ style?: StyleProp<ViewStyle>,
51
+ trackColorStyle?: StyleOverwrite<SwitchState, Color>,
52
+ borderColorStyle?: StyleOverwrite<SwitchState, Color>,
53
+ thumbColorStyle?: StyleOverwrite<SwitchState, Color>,
54
+ }
55
+
56
+ export const Switch = ({
57
+ value: controlledValue,
58
+ initialValue = false,
59
+ invalid = false,
60
+ disabled = false,
61
+ readOnly = false,
62
+ onValueChange,
63
+ onEditComplete,
64
+ style,
65
+ trackColorStyle,
66
+ borderColorStyle,
67
+ thumbColorStyle,
68
+ accessibilityLabel,
69
+ ...props
70
+ }: SwitchProps) => {
71
+ const { theme } = useTheme()
72
+ const interactive = !disabled && !readOnly
73
+
74
+ const onEditCompleteStable = useEventCallbackStabilizer(onEditComplete)
75
+ const onValueChangeStable = useEventCallbackStabilizer(onValueChange)
76
+
77
+ const onChangeWrapper = useCallback((nextValue: boolean) => {
78
+ onValueChangeStable(nextValue)
79
+ onEditCompleteStable(nextValue)
80
+ }, [onEditCompleteStable, onValueChangeStable])
81
+
82
+ const [value, setValue] = useControlledState({
83
+ value: controlledValue,
84
+ onValueChange: onChangeWrapper,
85
+ defaultValue: initialValue,
86
+ })
87
+
88
+ const state = useMemo((): SwitchState => ({
89
+ isActive: value,
90
+ isInvalid: invalid,
91
+ isDisabled: disabled,
92
+ isReadOnly: readOnly,
93
+ }), [disabled, invalid, readOnly, value])
94
+
95
+ const trackColor = useMemo(
96
+ () => theme.components.switch.trackColor(state, trackColorStyle),
97
+ [theme, state, trackColorStyle]
98
+ )
99
+ const borderColor = useMemo(
100
+ () => theme.components.switch.borderColor(state, borderColorStyle),
101
+ [theme, state, borderColorStyle]
102
+ )
103
+ const thumbColor = useMemo(
104
+ () => theme.components.switch.thumbColor(state, thumbColorStyle),
105
+ [theme, state, thumbColorStyle]
106
+ )
107
+
108
+ const progress = useRef(new Animated.Value(value ? 1 : 0)).current
109
+
110
+ useEffect(() => {
111
+ Animated.timing(progress, {
112
+ toValue: value ? 1 : 0,
113
+ duration: ANIMATION_DURATION_MS,
114
+ useNativeDriver: false,
115
+ }).start()
116
+ }, [progress, value])
117
+
118
+ const thumbSize = progress.interpolate({
119
+ inputRange: [0, 1],
120
+ outputRange: [THUMB_SIZE_INACTIVE, THUMB_SIZE_ACTIVE],
121
+ })
122
+ const thumbTranslateX = progress.interpolate({
123
+ inputRange: [0, 1],
124
+ outputRange: [THUMB_OFFSET_INACTIVE, THUMB_OFFSET_ACTIVE],
125
+ })
126
+ const thumbTop = progress.interpolate({
127
+ inputRange: [0, 1],
128
+ outputRange: [THUMB_TOP_INACTIVE, THUMB_TOP_ACTIVE],
129
+ })
130
+
131
+ return (
132
+ <Pressable
133
+ {...props}
134
+ disabled={!interactive}
135
+ accessibilityRole="switch"
136
+ accessibilityLabel={accessibilityLabel}
137
+ accessibilityState={{
138
+ checked: value,
139
+ disabled,
140
+ }}
141
+ onPress={(event) => {
142
+ if (interactive) {
143
+ setValue((previous) => !previous)
144
+ }
145
+ props.onPress?.(event)
146
+ }}
147
+ style={style}
148
+ >
149
+ <View
150
+ style={{
151
+ width: TRACK_WIDTH,
152
+ height: TRACK_HEIGHT,
153
+ borderRadius: TRACK_HEIGHT / 2,
154
+ borderWidth: TRACK_BORDER_WIDTH,
155
+ borderColor,
156
+ backgroundColor: trackColor,
157
+ padding: TRACK_PADDING,
158
+ opacity: disabled ? 0.6 : 1,
159
+ }}
160
+ >
161
+ <View style={{ flex: 1 }}>
162
+ <Animated.View
163
+ style={{
164
+ position: 'absolute',
165
+ top: thumbTop,
166
+ width: thumbSize,
167
+ height: thumbSize,
168
+ borderRadius: THUMB_SIZE_ACTIVE / 2,
169
+ backgroundColor: thumbColor,
170
+ transform: [{ translateX: thumbTranslateX }],
171
+ }}
172
+ />
173
+ </View>
174
+ </View>
175
+ </Pressable>
176
+ )
177
+ }
@@ -4,3 +4,4 @@ export * from './IconButton'
4
4
  export * from './Input'
5
5
  export * from './MultiSelect'
6
6
  export * from './Select'
7
+ export * from './Switch'
@@ -9,3 +9,4 @@ export * from './input'
9
9
  export * from './menu'
10
10
  export * from './multiSelect'
11
11
  export * from './select'
12
+ export * from './switch'
@@ -0,0 +1,65 @@
1
+ import type {
2
+ ComponentColorTokens,
3
+ HightideThemeTokens as DesignTokensTheme
4
+ } from '@helpwave/hightide-design/types'
5
+
6
+ import type { HightideSemanticColors } from '../types/color'
7
+ import type {
8
+ SwitchState,
9
+ SwitchTheme
10
+ } from '../types/components/switch'
11
+ import { createValueResolver } from '../types/resolver'
12
+
13
+ export type CreateSwitchThemeOptions = {
14
+ semantic: HightideSemanticColors,
15
+ component: ComponentColorTokens,
16
+ }
17
+
18
+ export const createSwitchTheme = ({
19
+ semantic,
20
+ component,
21
+ }: CreateSwitchThemeOptions): SwitchTheme => {
22
+ const resolveState = (state: SwitchState) => {
23
+ const trackInactive = state.isDisabled
24
+ ? semantic.disabled
25
+ : component.switch.track.inactive
26
+ const trackActive = state.isDisabled
27
+ ? semantic.disabled
28
+ : component.switch.track.active
29
+
30
+ const trackColor = state.isActive ? trackActive : trackInactive
31
+
32
+ const borderColor = state.isDisabled
33
+ ? semantic.disabled
34
+ : state.isInvalid
35
+ ? semantic.negative
36
+ : state.isActive
37
+ ? trackActive
38
+ : component.switch.borderColor
39
+
40
+ const thumbColor = state.isDisabled
41
+ ? semantic.onDisabled
42
+ : state.isActive
43
+ ? component.switch.thumb.active
44
+ : component.switch.thumb.inactive
45
+
46
+ return {
47
+ trackColor,
48
+ borderColor,
49
+ thumbColor,
50
+ }
51
+ }
52
+
53
+ return {
54
+ trackColor: createValueResolver((state) => resolveState(state).trackColor),
55
+ borderColor: createValueResolver((state) => resolveState(state).borderColor),
56
+ thumbColor: createValueResolver((state) => resolveState(state).thumbColor),
57
+ }
58
+ }
59
+
60
+ export const createSwitchThemeFromDesign = (theme: DesignTokensTheme): SwitchTheme => {
61
+ return createSwitchTheme({
62
+ semantic: theme.semanticColors,
63
+ component: theme.componentColors,
64
+ })
65
+ }
@@ -8,6 +8,7 @@ import { createButtonThemeFromDesign } from '../resolvers/button'
8
8
  import { createChatThemeFromDesign } from '../resolvers/chat'
9
9
  import { createCheckboxThemeFromDesign } from '../resolvers/checkbox'
10
10
  import { createChipThemeFromDesign } from '../resolvers/chip'
11
+ import { createSwitchThemeFromDesign } from '../resolvers/switch'
11
12
  import { createIconButtonThemeFromDesign } from '../resolvers/iconButton'
12
13
  import { createInputThemeFromDesign } from '../resolvers/input'
13
14
  import { createMenuThemeFromDesign } from '../resolvers/menu'
@@ -47,6 +48,7 @@ export const createHightideTheme = (tokens: HightideThemeTokens): HightideTheme
47
48
  iconButton: createIconButtonThemeFromDesign(tokens),
48
49
  chip: createChipThemeFromDesign(tokens),
49
50
  checkbox: createCheckboxThemeFromDesign(tokens),
51
+ switch: createSwitchThemeFromDesign(tokens),
50
52
  input: createInputThemeFromDesign(tokens),
51
53
  select: createSelectThemeFromDesign(tokens),
52
54
  multiSelect: createMultiSelectThemeFromDesign(tokens),
@@ -9,6 +9,7 @@ import type { InputTheme } from './input'
9
9
  import type { MenuTheme } from './menu'
10
10
  import type { MultiSelectTheme } from './multiSelect'
11
11
  import type { SelectTheme } from './select'
12
+ import type { SwitchTheme } from './switch'
12
13
 
13
14
  export type ColoringDefinition = {
14
15
  color: Color,
@@ -35,6 +36,7 @@ export type HightideComponentThemes = {
35
36
  iconButton: IconButtonTheme,
36
37
  chip: ChipTheme,
37
38
  checkbox: CheckboxTheme,
39
+ switch: SwitchTheme,
38
40
  input: InputTheme,
39
41
  select: SelectTheme,
40
42
  multiSelect: MultiSelectTheme,
@@ -9,3 +9,4 @@ export * from './input'
9
9
  export * from './menu'
10
10
  export * from './multiSelect'
11
11
  export * from './select'
12
+ export * from './switch'
@@ -0,0 +1,15 @@
1
+ import type { Color } from '../color'
2
+ import type {
3
+ InteractionState,
4
+ StyleResolverFunction
5
+ } from '../resolver'
6
+
7
+ export type SwitchState = InteractionState & {
8
+ isActive?: boolean,
9
+ }
10
+
11
+ export type SwitchTheme = {
12
+ trackColor: StyleResolverFunction<SwitchState, Color>,
13
+ borderColor: StyleResolverFunction<SwitchState, Color>,
14
+ thumbColor: StyleResolverFunction<SwitchState, Color>,
15
+ }