@hzblj/kara-components 0.8.0 → 0.10.0
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/CHANGELOG.md +60 -26
- package/README.md +8 -8
- package/lib/module/atoms/color-picker/color-picker.android.js +32 -5
- package/lib/module/atoms/color-picker/color-picker.android.js.map +1 -1
- package/lib/module/atoms/icon-button/icon-button.ios.js +17 -4
- package/lib/module/atoms/icon-button/icon-button.ios.js.map +1 -1
- package/lib/module/atoms/pressable/pressable.js +45 -22
- package/lib/module/atoms/pressable/pressable.js.map +1 -1
- package/lib/module/atoms/progress/progress.ios.js +10 -4
- package/lib/module/atoms/progress/progress.ios.js.map +1 -1
- package/lib/module/atoms/surface/surface.ios.js +3 -0
- package/lib/module/atoms/surface/surface.ios.js.map +1 -1
- package/lib/module/atoms/text-field/text-field.android.js +19 -4
- package/lib/module/atoms/text-field/text-field.android.js.map +1 -1
- package/lib/module/atoms/text-field/text-field.ios.js +7 -2
- package/lib/module/atoms/text-field/text-field.ios.js.map +1 -1
- package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +1 -1
- package/lib/typescript/atoms/avatar/avatar.types.d.ts +2 -2
- package/lib/typescript/atoms/badge/badge.types.d.ts +1 -1
- package/lib/typescript/atoms/blur-view/blur-view.types.d.ts +6 -6
- package/lib/typescript/atoms/button/button.types.d.ts +7 -7
- package/lib/typescript/atoms/card/card.types.d.ts +1 -1
- package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +2 -2
- package/lib/typescript/atoms/chip/chip.types.d.ts +1 -1
- package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +4 -3
- package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +4 -4
- package/lib/typescript/atoms/divider/divider.types.d.ts +2 -2
- package/lib/typescript/atoms/fab/fab.types.d.ts +2 -2
- package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +5 -5
- package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +3 -3
- package/lib/typescript/atoms/icon/icon.types.d.ts +4 -4
- package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +7 -3
- package/lib/typescript/atoms/island/island.types.d.ts +4 -4
- package/lib/typescript/atoms/list/list.types.d.ts +5 -5
- package/lib/typescript/atoms/picker/picker.types.d.ts +1 -1
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +21 -5
- package/lib/typescript/atoms/progress/progress.types.d.ts +2 -2
- package/lib/typescript/atoms/rn-flat-list/rn-flat-list.types.d.ts +2 -2
- package/lib/typescript/atoms/rn-icon/rn-icon.types.d.ts +1 -1
- package/lib/typescript/atoms/rn-scroll-view/rn-scroll-view.types.d.ts +2 -2
- package/lib/typescript/atoms/rn-section-list/rn-section-list.types.d.ts +2 -2
- package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +2 -2
- package/lib/typescript/atoms/rn-view/rn-view.types.d.ts +3 -3
- package/lib/typescript/atoms/scroll-view/scroll-view.types.d.ts +3 -3
- package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +3 -3
- package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +2 -2
- package/lib/typescript/atoms/spacer/spacer.types.d.ts +1 -1
- package/lib/typescript/atoms/surface/surface.types.d.ts +2 -2
- package/lib/typescript/atoms/switch/switch.types.d.ts +1 -1
- package/lib/typescript/atoms/text/text.types.d.ts +1 -1
- package/lib/typescript/atoms/text-field/text-field.types.d.ts +8 -4
- package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +1 -1
- package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +1 -1
- package/lib/typescript/glyph/glyph-button.types.d.ts +1 -1
- package/lib/typescript/glyph/glyph.types.d.ts +1 -1
- package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +1 -1
- package/lib/typescript/molecules/empty-state/empty-state.types.d.ts +1 -1
- package/lib/typescript/molecules/label/label.types.d.ts +1 -1
- package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +2 -2
- package/lib/typescript/molecules/list-item/list-item.types.d.ts +2 -2
- package/lib/typescript/molecules/navigation-bar/navigation-bar.types.d.ts +8 -8
- package/lib/typescript/molecules/pager/pager.types.d.ts +2 -2
- package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +2 -2
- package/lib/typescript/molecules/rn-list-item/rn-list-item.types.d.ts +1 -1
- package/lib/typescript/molecules/rn-tooltip/rn-tooltip.types.d.ts +7 -7
- package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +3 -3
- package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +2 -2
- package/lib/typescript/molecules/stepper/stepper.types.d.ts +2 -2
- package/lib/typescript/molecules/tooltip/tooltip.types.d.ts +6 -6
- package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +3 -3
- package/lib/typescript/organisms/alert/alert.types.d.ts +3 -3
- package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +9 -9
- package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +5 -5
- package/lib/typescript/organisms/form/form.types.d.ts +1 -1
- package/lib/typescript/organisms/menu/menu.types.d.ts +2 -2
- package/lib/typescript/organisms/sheet/sheet.types.d.ts +12 -12
- package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +1 -1
- package/lib/typescript/organisms/swipe-actions/swipe-actions.types.d.ts +5 -5
- package/lib/typescript/templates/screen/screen.types.d.ts +2 -2
- package/package.json +4 -4
- package/src/atoms/activity-indicator/activity-indicator.types.ts +1 -1
- package/src/atoms/avatar/avatar.types.ts +2 -2
- package/src/atoms/badge/badge.types.ts +1 -1
- package/src/atoms/blur-view/blur-view.types.ts +6 -6
- package/src/atoms/button/button.types.ts +7 -7
- package/src/atoms/card/card.types.ts +1 -1
- package/src/atoms/checkbox/checkbox.types.ts +2 -2
- package/src/atoms/chip/chip.types.ts +1 -1
- package/src/atoms/color-picker/color-picker.android.tsx +26 -6
- package/src/atoms/color-picker/color-picker.types.ts +4 -3
- package/src/atoms/date-picker/date-picker.types.ts +4 -4
- package/src/atoms/divider/divider.types.ts +2 -2
- package/src/atoms/fab/fab.types.ts +2 -2
- package/src/atoms/glass-view/glass-view.types.ts +5 -5
- package/src/atoms/h-stack/h-stack.types.ts +3 -3
- package/src/atoms/icon/icon.types.ts +4 -4
- package/src/atoms/icon-button/icon-button.ios.tsx +12 -2
- package/src/atoms/icon-button/icon-button.types.ts +7 -3
- package/src/atoms/island/island.types.ts +4 -4
- package/src/atoms/list/list.types.ts +5 -5
- package/src/atoms/picker/picker.types.ts +1 -1
- package/src/atoms/pressable/pressable.tsx +39 -18
- package/src/atoms/pressable/pressable.types.ts +22 -5
- package/src/atoms/progress/progress.ios.tsx +14 -5
- package/src/atoms/progress/progress.types.ts +2 -2
- package/src/atoms/rn-flat-list/rn-flat-list.types.ts +2 -2
- package/src/atoms/rn-icon/rn-icon.types.ts +1 -1
- package/src/atoms/rn-scroll-view/rn-scroll-view.types.ts +2 -2
- package/src/atoms/rn-section-list/rn-section-list.types.ts +2 -2
- package/src/atoms/rn-text/rn-text.types.ts +2 -2
- package/src/atoms/rn-view/rn-view.types.ts +3 -3
- package/src/atoms/scroll-view/scroll-view.types.ts +3 -3
- package/src/atoms/segmented-control/segmented-control.types.ts +3 -3
- package/src/atoms/skeleton/skeleton.types.ts +2 -2
- package/src/atoms/spacer/spacer.types.ts +1 -1
- package/src/atoms/surface/surface.ios.tsx +2 -1
- package/src/atoms/surface/surface.types.ts +2 -2
- package/src/atoms/switch/switch.types.ts +1 -1
- package/src/atoms/text/text.types.ts +1 -1
- package/src/atoms/text-field/text-field.android.tsx +19 -3
- package/src/atoms/text-field/text-field.ios.tsx +7 -1
- package/src/atoms/text-field/text-field.types.ts +8 -4
- package/src/atoms/v-stack/v-stack.types.ts +1 -1
- package/src/atoms/z-stack/z-stack.types.ts +1 -1
- package/src/glyph/glyph-button.types.ts +1 -1
- package/src/glyph/glyph.types.ts +1 -1
- package/src/molecules/collapsible/collapsible.types.ts +1 -1
- package/src/molecules/empty-state/empty-state.types.ts +1 -1
- package/src/molecules/label/label.types.ts +1 -1
- package/src/molecules/labeled-content/labeled-content.types.ts +2 -2
- package/src/molecules/list-item/list-item.types.ts +2 -2
- package/src/molecules/navigation-bar/navigation-bar.types.ts +8 -8
- package/src/molecules/pager/pager.types.ts +2 -2
- package/src/molecules/radio-group/radio-group.types.ts +2 -2
- package/src/molecules/rn-list-item/rn-list-item.types.ts +1 -1
- package/src/molecules/rn-tooltip/rn-tooltip.types.ts +7 -7
- package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +3 -3
- package/src/molecules/search-bar/search-bar.types.ts +2 -2
- package/src/molecules/stepper/stepper.types.ts +2 -2
- package/src/molecules/tooltip/tooltip.types.ts +6 -6
- package/src/organisms/action-sheet/action-sheet.types.ts +3 -3
- package/src/organisms/alert/alert.types.ts +3 -3
- package/src/organisms/context-menu/context-menu.types.ts +9 -9
- package/src/organisms/fab-menu/fab-menu.types.ts +5 -5
- package/src/organisms/form/form.types.ts +1 -1
- package/src/organisms/menu/menu.types.ts +2 -2
- package/src/organisms/sheet/sheet.types.ts +12 -12
- package/src/organisms/snackbar/snackbar.types.ts +1 -1
- package/src/organisms/swipe-actions/swipe-actions.types.ts +5 -5
- package/src/templates/screen/screen.types.ts +2 -2
- package/lib/module/platform-support.js +0 -19
- package/lib/module/platform-support.js.map +0 -1
- package/lib/typescript/platform-support.d.ts +0 -8
- package/src/platform-support.ts +0 -20
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {useSplitSx} from '@hzblj/kara-sx'
|
|
2
|
-
import {useAppTheme, withOpacity} from '@hzblj/kara-theme'
|
|
2
|
+
import {duration, opacity, useAppTheme, withOpacity} from '@hzblj/kara-theme'
|
|
3
3
|
import {useCallback, useMemo} from 'react'
|
|
4
|
-
import {Platform, Pressable as RNPressable,
|
|
5
|
-
import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'
|
|
4
|
+
import {Platform, Pressable as RNPressable, type ViewStyle} from 'react-native'
|
|
5
|
+
import Animated, {useAnimatedStyle, useSharedValue, withSpring, withTiming} from 'react-native-reanimated'
|
|
6
6
|
|
|
7
7
|
import {haptics} from '../../haptics'
|
|
8
8
|
import {splitForRipple} from '../../ripple-clip'
|
|
@@ -10,6 +10,12 @@ import type {PressableComponent, PressableProps} from './pressable.types'
|
|
|
10
10
|
|
|
11
11
|
const SPRING = {damping: 20, stiffness: 400}
|
|
12
12
|
|
|
13
|
+
/** A dim comes in quickly and leaves a little slower, as UIKit's own highlight does. */
|
|
14
|
+
const DIM_IN = {duration: duration.d100}
|
|
15
|
+
const DIM_OUT = {duration: duration.d200}
|
|
16
|
+
|
|
17
|
+
const PLATFORM_EFFECT = Platform.OS === 'ios' ? 'scale' : 'none'
|
|
18
|
+
|
|
13
19
|
const AnimatedPressable = Animated.createAnimatedComponent(RNPressable)
|
|
14
20
|
|
|
15
21
|
export const Pressable: PressableComponent = ({
|
|
@@ -17,24 +23,38 @@ export const Pressable: PressableComponent = ({
|
|
|
17
23
|
disabled = false,
|
|
18
24
|
haptic = false,
|
|
19
25
|
onPress,
|
|
26
|
+
pressEffect = PLATFORM_EFFECT,
|
|
27
|
+
pressedOpacity = 'o60',
|
|
28
|
+
pressedScale = 0.96,
|
|
29
|
+
ripple: withRipple = true,
|
|
20
30
|
testID,
|
|
21
31
|
...props
|
|
22
32
|
}: PressableProps) => {
|
|
23
33
|
const [resolved, rest] = useSplitSx(props)
|
|
24
|
-
|
|
34
|
+
// 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
|
|
35
|
+
const pressed = useSharedValue(0)
|
|
25
36
|
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
37
|
+
const dimmed = opacity[pressedOpacity]
|
|
26
38
|
|
|
27
39
|
const theme = useAppTheme()
|
|
28
40
|
const isAndroid = Platform.OS === 'android'
|
|
29
41
|
|
|
30
|
-
const rStyle = useAnimatedStyle(() =>
|
|
42
|
+
const rStyle = useAnimatedStyle(() => {
|
|
43
|
+
if (pressEffect === 'scale') {
|
|
44
|
+
return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
|
|
45
|
+
}
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
return pressEffect === 'opacity' ? {opacity: 1 - (1 - dimmed) * pressed.value} : {}
|
|
48
|
+
}, [dimmed, pressEffect, pressedScale])
|
|
49
|
+
|
|
50
|
+
// Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
|
|
51
|
+
// the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
|
|
34
52
|
const ripple = useMemo(
|
|
35
53
|
() =>
|
|
36
|
-
isAndroid
|
|
37
|
-
|
|
54
|
+
isAndroid && withRipple
|
|
55
|
+
? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
|
|
56
|
+
: undefined,
|
|
57
|
+
[isAndroid, theme, withRipple]
|
|
38
58
|
)
|
|
39
59
|
const androidStyle = useMemo(
|
|
40
60
|
() => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
|
|
@@ -43,20 +63,20 @@ export const Pressable: PressableComponent = ({
|
|
|
43
63
|
|
|
44
64
|
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
45
65
|
const onPressIn = useCallback(() => {
|
|
46
|
-
if (
|
|
47
|
-
|
|
66
|
+
if (pressEffect === 'scale') {
|
|
67
|
+
pressed.value = withSpring(1, SPRING)
|
|
68
|
+
} else if (pressEffect === 'opacity') {
|
|
69
|
+
pressed.value = withTiming(1, DIM_IN)
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
if (feedback) {
|
|
51
73
|
haptics.prepare(feedback)
|
|
52
74
|
}
|
|
53
|
-
}, [feedback,
|
|
75
|
+
}, [feedback, pressEffect, pressed])
|
|
54
76
|
|
|
55
77
|
const onPressOut = useCallback(() => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
}, [isAndroid, scale])
|
|
78
|
+
pressed.value = pressEffect === 'scale' ? withSpring(0, SPRING) : withTiming(0, DIM_OUT)
|
|
79
|
+
}, [pressEffect, pressed])
|
|
60
80
|
|
|
61
81
|
const onPressWithHaptic = useCallback(() => {
|
|
62
82
|
if (feedback) {
|
|
@@ -68,20 +88,21 @@ export const Pressable: PressableComponent = ({
|
|
|
68
88
|
|
|
69
89
|
if (androidStyle) {
|
|
70
90
|
return (
|
|
71
|
-
<View style={androidStyle.outer}>
|
|
91
|
+
<Animated.View style={[androidStyle.outer, rStyle]}>
|
|
72
92
|
<RNPressable
|
|
73
93
|
android_ripple={ripple}
|
|
74
94
|
disabled={disabled}
|
|
75
95
|
hitSlop={8}
|
|
76
96
|
onPress={onPressWithHaptic}
|
|
77
97
|
onPressIn={onPressIn}
|
|
98
|
+
onPressOut={onPressOut}
|
|
78
99
|
style={androidStyle.inner}
|
|
79
100
|
testID={testID}
|
|
80
101
|
{...rest}
|
|
81
102
|
>
|
|
82
103
|
{children}
|
|
83
104
|
</RNPressable>
|
|
84
|
-
</View>
|
|
105
|
+
</Animated.View>
|
|
85
106
|
)
|
|
86
107
|
}
|
|
87
108
|
|
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
2
|
+
import type {OpacitySize} from '@hzblj/kara-theme'
|
|
2
3
|
import type {ReactNode} from 'react'
|
|
3
4
|
|
|
4
5
|
import type {Haptic} from '../../haptics'
|
|
5
6
|
|
|
7
|
+
/** How the content answers a press: it springs down, it dims, or it stays still. */
|
|
8
|
+
export type PressEffect = 'none' | 'opacity' | 'scale'
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
|
-
* React Native's `Pressable`, styled through `sx` like a `Box`, taking touches 8 points past its edges.
|
|
8
|
-
*
|
|
9
|
-
* pressable's shape
|
|
10
|
-
*
|
|
11
|
+
* React Native's `Pressable`, styled through `sx` like a `Box`, taking touches 8 points past its edges. Each platform
|
|
12
|
+
* presses its own way by default: iOS springs the content down to 96%, Android draws Material's ripple over it,
|
|
13
|
+
* clipped to the pressable's shape. `pressEffect` picks the same answer on both, and Android keeps its ripple on top
|
|
14
|
+
* unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`). The React Native
|
|
15
|
+
* tier: it needs no host.
|
|
11
16
|
*/
|
|
12
17
|
export type PressableProps = ResponsiveViewSx & {
|
|
13
18
|
/** The pressable content. */
|
|
14
19
|
children?: ReactNode
|
|
15
|
-
/** Ignores presses
|
|
20
|
+
/** Ignores presses: no `onPress`, no haptic, no press effect or ripple. */
|
|
16
21
|
disabled?: boolean
|
|
17
22
|
/** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
|
|
18
23
|
haptic?: boolean | Haptic
|
|
19
24
|
/** Called when the press is released, after the `haptic` plays. */
|
|
20
25
|
onPress?: () => void
|
|
26
|
+
/**
|
|
27
|
+
* How the content answers while it is held. `scale` springs it down to `pressedScale`, `opacity` dims it to
|
|
28
|
+
* `pressedOpacity`, `none` leaves it still. `scale` on iOS and `none` on Android by default, where the ripple is the
|
|
29
|
+
* press.
|
|
30
|
+
*/
|
|
31
|
+
pressEffect?: PressEffect
|
|
32
|
+
/** What `opacity` dims to, an opacity token. `o60` by default. */
|
|
33
|
+
pressedOpacity?: OpacitySize
|
|
34
|
+
/** What `scale` springs down to. `0.96` by default. */
|
|
35
|
+
pressedScale?: number
|
|
36
|
+
/** Android's ripple: Material's pressed state layer over the content, clipped to the shape. On by default. iOS has none. */
|
|
37
|
+
ripple?: boolean
|
|
21
38
|
/** Test identifier, forwarded to the view. */
|
|
22
39
|
testID?: string
|
|
23
40
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {ProgressView} from '@expo/ui/swift-ui'
|
|
2
|
-
import {progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers'
|
|
1
|
+
import {Gauge, ProgressView} from '@expo/ui/swift-ui'
|
|
2
|
+
import {gaugeStyle, progressViewStyle, tint} from '@expo/ui/swift-ui/modifiers'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
4
|
import {useMemo} from 'react'
|
|
5
5
|
|
|
@@ -10,10 +10,19 @@ export const Progress: ProgressComponent = ({circular = false, value}: ProgressP
|
|
|
10
10
|
useHostGuard('Progress')
|
|
11
11
|
const theme = useAppTheme()
|
|
12
12
|
|
|
13
|
+
const ring = circular && value !== undefined
|
|
13
14
|
const modifiers = useMemo(
|
|
14
|
-
() => [
|
|
15
|
-
|
|
15
|
+
() => [
|
|
16
|
+
ring ? gaugeStyle('circularCapacity') : progressViewStyle(circular ? 'circular' : 'linear'),
|
|
17
|
+
tint(theme.color('fill.accent.primary')),
|
|
18
|
+
],
|
|
19
|
+
[circular, ring, theme]
|
|
16
20
|
)
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
// SwiftUI's circular `ProgressView` ignores its value on iOS and spins; a ring that fills is a capacity `Gauge`.
|
|
23
|
+
return ring ? (
|
|
24
|
+
<Gauge modifiers={modifiers} value={value} />
|
|
25
|
+
) : (
|
|
26
|
+
<ProgressView modifiers={modifiers} value={value ?? null} />
|
|
27
|
+
)
|
|
19
28
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A progress bar or ring, tinted `fill.accent.primary
|
|
5
|
-
* `LinearProgressIndicator` and `CircularProgressIndicator`.
|
|
4
|
+
* A progress bar or ring, tinted `fill.accent.primary`: SwiftUI `ProgressView` (a capacity `Gauge` for a ring with a
|
|
5
|
+
* value, which `ProgressView` cannot draw on iOS) / Compose's `LinearProgressIndicator` and `CircularProgressIndicator`.
|
|
6
6
|
*/
|
|
7
7
|
export type ProgressProps = {
|
|
8
8
|
/** A circular indicator instead of a bar. Off by default. */
|
|
@@ -3,11 +3,11 @@ import type {FlatListProps as RNFlatListProps} from 'react-native'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Top-level `sx` styles the viewport; `content` styles the scrolling content container, so padding and
|
|
6
|
-
* gap belong there
|
|
6
|
+
* gap belong there (the same split as `RnScrollView`). `removeClippedSubviews` is off unless you turn it on:
|
|
7
7
|
* Android's default would detach a row's host before it has measured itself, and the row would stay empty.
|
|
8
8
|
*/
|
|
9
9
|
export type RnFlatListProps<TItem> = ResponsiveViewSx &
|
|
10
10
|
Omit<RNFlatListProps<TItem>, 'contentContainerStyle' | 'style'> & {
|
|
11
|
-
/** Styles the scrolling content container
|
|
11
|
+
/** Styles the scrolling content container; padding and gap belong here. */
|
|
12
12
|
content?: ResponsiveViewSx
|
|
13
13
|
}
|
|
@@ -2,7 +2,7 @@ import type {ReactNode} from 'react'
|
|
|
2
2
|
|
|
3
3
|
import type {IconProps} from '../icon'
|
|
4
4
|
|
|
5
|
-
/** The same contract as `Icon`, for the React Native tier
|
|
5
|
+
/** The same contract as `Icon`, for the React Native tier: anywhere, no `Screen` or `Island` needed. */
|
|
6
6
|
export type RnIconProps = IconProps
|
|
7
7
|
|
|
8
8
|
export type RnIconComponent = (props: RnIconProps) => ReactNode
|
|
@@ -4,7 +4,7 @@ import type {ScrollViewProps as RNScrollViewProps} from 'react-native'
|
|
|
4
4
|
import type {ScrollViewAxis} from '../scroll-view'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* React Native tier scroll container
|
|
7
|
+
* React Native tier scroll container, the sibling of `RnView`/`RnText`. Use it when the
|
|
8
8
|
* subtree is React Native and native atoms are embedded through `Island`. For a screen that
|
|
9
9
|
* is native all the way down, reach for the `ScrollView` atom instead.
|
|
10
10
|
*
|
|
@@ -17,7 +17,7 @@ export type RnScrollViewProps = ResponsiveViewSx &
|
|
|
17
17
|
axis?: ScrollViewAxis
|
|
18
18
|
/** The scrolling content. */
|
|
19
19
|
children?: ReactNode
|
|
20
|
-
/** Styles the scrolling content container
|
|
20
|
+
/** Styles the scrolling content container; padding and gap belong here. */
|
|
21
21
|
content?: ResponsiveViewSx
|
|
22
22
|
/** Shows the scroll indicators. On by default. */
|
|
23
23
|
showsIndicators?: boolean
|
|
@@ -3,11 +3,11 @@ import type {DefaultSectionT, SectionListProps as RNSectionListProps} from 'reac
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Top-level `sx` styles the viewport; `content` styles the scrolling content container, so padding and
|
|
6
|
-
* gap belong there
|
|
6
|
+
* gap belong there (the same split as `RnScrollView` and `RnFlatList`). `removeClippedSubviews` is off unless
|
|
7
7
|
* you turn it on, as in `RnFlatList`: Android's default would leave rows with hosts in them empty.
|
|
8
8
|
*/
|
|
9
9
|
export type RnSectionListProps<TItem, TSection = DefaultSectionT> = ResponsiveViewSx &
|
|
10
10
|
Omit<RNSectionListProps<TItem, TSection>, 'contentContainerStyle' | 'style'> & {
|
|
11
|
-
/** Styles the scrolling content container
|
|
11
|
+
/** Styles the scrolling content container; padding and gap belong here. */
|
|
12
12
|
content?: ResponsiveViewSx
|
|
13
13
|
}
|
|
@@ -4,8 +4,8 @@ import type {ReactNode} from 'react'
|
|
|
4
4
|
import type {TextProps as RNTextProps} from 'react-native'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* React Native's `Text` in a theme typography variant
|
|
8
|
-
* `Box`'s `sx` surface for its own box
|
|
7
|
+
* React Native's `Text` in a theme typography variant: the React Native tier, for text outside a native host. It takes
|
|
8
|
+
* `Box`'s `sx` surface for its own box (margins, flex, padding, a background), all but `align`, which here aligns the
|
|
9
9
|
* lines.
|
|
10
10
|
*/
|
|
11
11
|
export type RnTextProps = Omit<RNTextProps, 'style'> &
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type {ReactElement, ReactNode} from 'react'
|
|
2
2
|
|
|
3
|
-
/** Carries a React Native view into a SwiftUI or Compose tree
|
|
3
|
+
/** Carries a React Native view into a SwiftUI or Compose tree: `@expo/ui`'s `RNHostView` on both platforms. */
|
|
4
4
|
export type RnViewProps = {
|
|
5
5
|
/**
|
|
6
6
|
* A single React Native *view*. It may not contain React Native text: a bare string becomes a
|
|
7
7
|
* `RawText` node, which has no component view, so mounting it across the SwiftUI host boundary
|
|
8
8
|
* fails the `RCTComponentViewFactory` assertion and takes the whole app down. `@expo/ui` states
|
|
9
|
-
* the rule itself in `ListItem.ios.tsx`
|
|
9
|
+
* the rule itself in `ListItem.ios.tsx` ("SwiftUI hosts can't render bare strings") and wraps
|
|
10
10
|
* every raw string in a SwiftUI `Text` before it reaches a host. Do the same: use the `Text` atom
|
|
11
11
|
* inside the host, and keep `RnText` outside it.
|
|
12
12
|
*/
|
|
@@ -16,7 +16,7 @@ export type RnViewProps = {
|
|
|
16
16
|
* intrinsic size and would otherwise stretch to fill the SwiftUI parent.
|
|
17
17
|
*/
|
|
18
18
|
matchContents?: boolean
|
|
19
|
-
/** Not forwarded on either platform
|
|
19
|
+
/** Not forwarded on either platform: give the child its own `testID`. */
|
|
20
20
|
testID?: string
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -5,7 +5,7 @@ import type {ReactNode} from 'react'
|
|
|
5
5
|
export type ScrollViewAxis = 'horizontal' | 'vertical'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* The `sx` props style the scrolling *content* container, not the viewport
|
|
8
|
+
* The `sx` props style the scrolling *content* container, not the viewport: `p` behaves
|
|
9
9
|
* like React Native's `contentContainerStyle` padding and scrolls with the content.
|
|
10
10
|
*/
|
|
11
11
|
export type ScrollViewProps = ResponsiveSx & {
|
|
@@ -15,13 +15,13 @@ export type ScrollViewProps = ResponsiveSx & {
|
|
|
15
15
|
children?: ReactNode
|
|
16
16
|
/**
|
|
17
17
|
* Enables pull-to-refresh. Async on purpose: the spinner runs until the promise settles, which is
|
|
18
|
-
* what SwiftUI's `refreshable` expects and what Compose's controlled indicator is driven from
|
|
18
|
+
* what SwiftUI's `refreshable` expects and what Compose's controlled indicator is driven from, so
|
|
19
19
|
* neither platform needs a separate `refreshing` flag to be kept in sync by the caller.
|
|
20
20
|
*
|
|
21
21
|
* Only a vertical scroll view can be pulled; on a horizontal one both platforms ignore it.
|
|
22
22
|
*/
|
|
23
23
|
onRefresh?: () => Promise<void>
|
|
24
|
-
/** iOS only
|
|
24
|
+
/** iOS only: Compose exposes no indicator toggle, so Android always shows the native one. */
|
|
25
25
|
showsIndicators?: boolean
|
|
26
26
|
/** The space between children. */
|
|
27
27
|
spacing?: SpaceSize
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A row of mutually exclusive segments, the selected one in the accent
|
|
4
|
+
* A row of mutually exclusive segments, the selected one in the accent: kara's own `UISegmentedControl` on iOS,
|
|
5
5
|
* Material's single-choice segmented buttons on Android. It needs no `Screen` or `Island`: iOS is a plain view, Android
|
|
6
6
|
* brings its own host. It takes the width it is given; the height is the platform's.
|
|
7
7
|
*/
|
|
@@ -9,13 +9,13 @@ export type SegmentedControlProps = {
|
|
|
9
9
|
/** Stops the user changing the segment, and dims the control. */
|
|
10
10
|
disabled?: boolean
|
|
11
11
|
/**
|
|
12
|
-
* Called with the index and label of the segment the user picked
|
|
12
|
+
* Called with the index and label of the segment the user picked, never for a tap on the one already selected.
|
|
13
13
|
* Android plays `selection`; iOS's control plays its own feedback.
|
|
14
14
|
*/
|
|
15
15
|
onChange?: (index: number, value: string) => void
|
|
16
16
|
/** The selected segment. Controlled: pass back what `onChange` reports. */
|
|
17
17
|
selectedIndex: number
|
|
18
|
-
/** Test identifier, on the whole control
|
|
18
|
+
/** Test identifier, on the whole control. Reach a segment by its label. */
|
|
19
19
|
testID?: string
|
|
20
20
|
/** The segments' labels, in order. */
|
|
21
21
|
values: string[]
|
|
@@ -3,8 +3,8 @@ import type {RadiiSize} from '@hzblj/kara-theme'
|
|
|
3
3
|
import type {ReactNode} from 'react'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* A loading placeholder that pulses in `fill.quaternary
|
|
7
|
-
* `sx` names: `w`, `h`, `borderRadius`.
|
|
6
|
+
* A loading placeholder that pulses in `fill.quaternary`. It is a React Native view, so it needs no host. Sized with
|
|
7
|
+
* the `sx` names: `w`, `h`, `borderRadius`.
|
|
8
8
|
*/
|
|
9
9
|
export type SkeletonProps = {
|
|
10
10
|
/** The corner radius. `small` by default. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {SpaceSize} from '@hzblj/kara-theme'
|
|
2
2
|
import type {ReactNode} from 'react'
|
|
3
3
|
|
|
4
|
-
/** Takes up the free space in a stack
|
|
4
|
+
/** Takes up the free space in a stack: SwiftUI `Spacer` / Compose `Spacer` with `weight(1)`. */
|
|
5
5
|
export type SpacerProps = {
|
|
6
6
|
/** The least room it takes. iOS holds it along the stack's axis; Android as a minimum height only. */
|
|
7
7
|
min?: SpaceSize
|
|
@@ -6,7 +6,8 @@ import type {SurfaceComponent, SurfaceProps} from './surface.types'
|
|
|
6
6
|
export const Surface: SurfaceComponent = ({children, glass = false, testID, ...sx}: SurfaceProps) => {
|
|
7
7
|
useHostGuard('Surface')
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// `surface.primary` unless told otherwise, as Material's `Surface` is on Android — without it a plain one drew nothing.
|
|
10
|
+
const modifiers = useSxModifiers({bg: 'surface.primary', borderRadius: 'large', p: 's16', ...sx})
|
|
10
11
|
const content = (
|
|
11
12
|
<VStack alignment="leading" modifiers={modifiers} testID={testID}>
|
|
12
13
|
{children}
|
|
@@ -2,8 +2,8 @@ import type {ResponsiveSx} from '@hzblj/kara-sx'
|
|
|
2
2
|
import type {ReactNode} from 'react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* A container padded `s16` and rounded `large` unless told otherwise
|
|
6
|
-
* `Surface` around a `Column` on Android.
|
|
5
|
+
* A container on `surface.primary`, padded `s16` and rounded `large` unless told otherwise: a SwiftUI `VStack` on
|
|
6
|
+
* iOS, Material's `Surface` around a `Column` on Android.
|
|
7
7
|
*/
|
|
8
8
|
export type SurfaceProps = ResponsiveSx & {
|
|
9
9
|
/** Stacked vertically, from the leading edge. */
|
|
@@ -5,7 +5,7 @@ export type SwitchProps = {
|
|
|
5
5
|
/** Stops the user flipping it, and dims it. */
|
|
6
6
|
disabled?: boolean
|
|
7
7
|
/**
|
|
8
|
-
* Shown beside the switch. On Android it makes a full-width row
|
|
8
|
+
* Shown beside the switch. On Android it makes a full-width row (label at the start, switch at the end) that
|
|
9
9
|
* toggles wherever it is tapped.
|
|
10
10
|
*/
|
|
11
11
|
label?: string
|
|
@@ -9,7 +9,7 @@ export type TextDecoration = 'none' | 'strikethrough' | 'underline'
|
|
|
9
9
|
export type TextTransform = 'lowercase' | 'uppercase'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* SwiftUI `Text` / Compose `Text` in a theme typography variant
|
|
12
|
+
* SwiftUI `Text` / Compose `Text` in a theme typography variant. A native atom: outside a host, use `RnText`. It takes
|
|
13
13
|
* the native `sx` surface too: padding, a frame (`wide` lines it up by `align`), a background, and the `ios` bucket's
|
|
14
14
|
* `contentTransition`, `animation` and `monospacedDigit` for a number that changes in place.
|
|
15
15
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {Column, Text as ComposeText, OutlinedTextField, useNativeState} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {useSxModifiers} from '@hzblj/kara-sx'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
+
import {Glyph} from '../../glyph'
|
|
4
5
|
import {useHostGuard} from '../../host-context'
|
|
5
6
|
import type {TextFieldComponent, TextFieldProps} from './text-field.types'
|
|
6
7
|
|
|
@@ -17,12 +18,14 @@ export const TextField: TextFieldComponent = ({
|
|
|
17
18
|
const theme = useAppTheme()
|
|
18
19
|
const value = useNativeState(defaultValue)
|
|
19
20
|
const modifiers = useSxModifiers({wide: true})
|
|
21
|
+
const invalid = Boolean(error)
|
|
22
|
+
const message = typeof error === 'string' && error ? error : undefined
|
|
20
23
|
|
|
21
24
|
return (
|
|
22
25
|
<Column verticalArrangement={{spacedBy: 4}}>
|
|
23
26
|
{label ? (
|
|
24
27
|
<ComposeText
|
|
25
|
-
color={theme.colorString(
|
|
28
|
+
color={theme.colorString(invalid ? 'content.destructive' : 'content.secondary')}
|
|
26
29
|
style={theme.typography.labelMedium}
|
|
27
30
|
>
|
|
28
31
|
{label}
|
|
@@ -30,7 +33,7 @@ export const TextField: TextFieldComponent = ({
|
|
|
30
33
|
) : null}
|
|
31
34
|
<OutlinedTextField
|
|
32
35
|
enabled={!disabled}
|
|
33
|
-
isError={
|
|
36
|
+
isError={invalid}
|
|
34
37
|
modifiers={modifiers}
|
|
35
38
|
onValueChange={onChangeText}
|
|
36
39
|
singleLine={!multiline}
|
|
@@ -41,7 +44,20 @@ export const TextField: TextFieldComponent = ({
|
|
|
41
44
|
}}
|
|
42
45
|
value={value}
|
|
43
46
|
visualTransformation={secure ? 'password' : 'none'}
|
|
44
|
-
|
|
47
|
+
>
|
|
48
|
+
{invalid ? (
|
|
49
|
+
<OutlinedTextField.TrailingIcon>
|
|
50
|
+
<Glyph color="content.destructive" name="error" />
|
|
51
|
+
</OutlinedTextField.TrailingIcon>
|
|
52
|
+
) : null}
|
|
53
|
+
{message ? (
|
|
54
|
+
<OutlinedTextField.SupportingText>
|
|
55
|
+
<ComposeText color={theme.colorString('content.destructive')} style={theme.typography.footnote}>
|
|
56
|
+
{message}
|
|
57
|
+
</ComposeText>
|
|
58
|
+
</OutlinedTextField.SupportingText>
|
|
59
|
+
) : null}
|
|
60
|
+
</OutlinedTextField>
|
|
45
61
|
</Column>
|
|
46
62
|
)
|
|
47
63
|
}
|
|
@@ -45,7 +45,8 @@ export const TextField: TextFieldComponent = ({
|
|
|
45
45
|
}: TextFieldProps) => {
|
|
46
46
|
useHostGuard('TextField')
|
|
47
47
|
const text = useNativeState(defaultValue)
|
|
48
|
-
const tone = TONE[toneOf(disabled, error)]
|
|
48
|
+
const tone = TONE[toneOf(disabled, Boolean(error))]
|
|
49
|
+
const message = tone === TONE.error && typeof error === 'string' && error ? error : undefined
|
|
49
50
|
|
|
50
51
|
const sxModifiers = useSxModifiers({
|
|
51
52
|
bg: tone.bg,
|
|
@@ -85,6 +86,11 @@ export const TextField: TextFieldComponent = ({
|
|
|
85
86
|
text={text}
|
|
86
87
|
/>
|
|
87
88
|
)}
|
|
89
|
+
{message ? (
|
|
90
|
+
<Text color="content.destructive" variant="footnote">
|
|
91
|
+
{message}
|
|
92
|
+
</Text>
|
|
93
|
+
) : null}
|
|
88
94
|
</VStack>
|
|
89
95
|
)
|
|
90
96
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type {ReactNode} from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* An uncontrolled text field with an optional label above it
|
|
4
|
+
* An uncontrolled text field with an optional label above it: SwiftUI `TextField` (`SecureField` when `secure`) /
|
|
5
5
|
* Compose `OutlinedTextField`.
|
|
6
6
|
*/
|
|
7
7
|
export type TextFieldProps = {
|
|
8
|
-
/** The text it starts with. Read once, on mount
|
|
8
|
+
/** The text it starts with. Read once, on mount: the field is uncontrolled. */
|
|
9
9
|
defaultValue?: string
|
|
10
10
|
/** Stops editing and dims the field. On iOS it outranks `error`. */
|
|
11
11
|
disabled?: boolean
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Marks the field invalid: a destructive outline and label. A message is shown under the field as well (on Android
|
|
14
|
+
* as Material's supporting text, beside its error icon), so the error never rests on colour alone; Material's error
|
|
15
|
+
* colour is a pale red in dark mode. `true` marks it without a message.
|
|
16
|
+
*/
|
|
17
|
+
error?: boolean | string
|
|
14
18
|
/** Shown above the field, in `labelMedium`. */
|
|
15
19
|
label?: string
|
|
16
20
|
/** Wraps the text and grows the field to fit it. iOS ignores it when `secure`. */
|
|
@@ -4,7 +4,7 @@ import type {ReactNode} from 'react'
|
|
|
4
4
|
|
|
5
5
|
export type VStackAlign = 'center' | 'end' | 'start'
|
|
6
6
|
|
|
7
|
-
/** Stacks its children top to bottom
|
|
7
|
+
/** Stacks its children top to bottom: SwiftUI `VStack` / Compose `Column`. */
|
|
8
8
|
export type VStackProps = ResponsiveSx & {
|
|
9
9
|
/** The children's horizontal alignment. `start` by default. */
|
|
10
10
|
align?: VStackAlign
|
|
@@ -3,7 +3,7 @@ import type {ReactNode} from 'react'
|
|
|
3
3
|
|
|
4
4
|
export type ZStackAlign = 'bottom' | 'center' | 'top'
|
|
5
5
|
|
|
6
|
-
/** Layers its children on one another
|
|
6
|
+
/** Layers its children on one another: SwiftUI `ZStack` / Compose `Box`. */
|
|
7
7
|
export type ZStackProps = ResponsiveSx & {
|
|
8
8
|
/** Where the children sit vertically; they are always centred horizontally. `center` by default. */
|
|
9
9
|
align?: ZStackAlign
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {GlyphName} from '@hzblj/kara-native'
|
|
2
2
|
import type {ReactNode} from 'react'
|
|
3
3
|
|
|
4
|
-
/** An icon button over one of kara's glyphs
|
|
4
|
+
/** An icon button over one of kara's glyphs, for the controls its own components build (a stepper, a bar). */
|
|
5
5
|
export type GlyphButtonProps = {
|
|
6
6
|
accessibilityLabel?: string
|
|
7
7
|
disabled?: boolean
|
package/src/glyph/glyph.types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type {ColorToken, SpaceSize} from '@hzblj/kara-theme'
|
|
|
3
3
|
import type {ReactNode} from 'react'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* One of the few glyphs kara's own components draw
|
|
6
|
+
* One of the few glyphs kara's own components draw: a search field's magnifier, a row's chevron. Internal: apps draw
|
|
7
7
|
* their own icons with `Icon` / `RnIcon`.
|
|
8
8
|
*/
|
|
9
9
|
export type GlyphProps = {
|
|
@@ -2,7 +2,7 @@ import type {ReactNode} from 'react'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A row that shows and hides its content: SwiftUI's `DisclosureGroup` on iOS; on Android a pressable row with a
|
|
5
|
-
* chevron over an animated reveal. A native atom
|
|
5
|
+
* chevron over an animated reveal. A native atom: it renders inside `<Screen>` or `<Island>`.
|
|
6
6
|
*/
|
|
7
7
|
export type CollapsibleProps = {
|
|
8
8
|
/** Shown while `expanded`. */
|
|
@@ -6,7 +6,7 @@ import type {IconName} from '../../atoms/icon'
|
|
|
6
6
|
* `action` takes a `Button` so the state can offer a way out.
|
|
7
7
|
*/
|
|
8
8
|
export type EmptyStateProps = {
|
|
9
|
-
/** Under the text
|
|
9
|
+
/** Under the text: a `Button`, say. */
|
|
10
10
|
action?: ReactNode
|
|
11
11
|
/** A second line under the title, in the secondary colour. */
|
|
12
12
|
description?: string
|
|
@@ -4,7 +4,7 @@ import type {ReactNode} from 'react'
|
|
|
4
4
|
import type {IconName} from '../../atoms/icon'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* An inline icon + text pair
|
|
7
|
+
* An inline icon + text pair: the thing you put inside a button, a menu row or a section
|
|
8
8
|
* header. For a full tappable row with a subtitle and chevron, use `ListItem`.
|
|
9
9
|
*/
|
|
10
10
|
export type LabelProps = {
|
|
@@ -3,7 +3,7 @@ import type {ReactNode} from 'react'
|
|
|
3
3
|
import type {IconName} from '../../atoms/icon'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* A label on the leading edge and its value on the trailing edge
|
|
6
|
+
* A label on the leading edge and its value on the trailing edge: the settings/detail row.
|
|
7
7
|
* Pass `children` instead of `value` when the trailing side is a control rather than text.
|
|
8
8
|
*/
|
|
9
9
|
export type LabeledContentProps = {
|
|
@@ -15,7 +15,7 @@ export type LabeledContentProps = {
|
|
|
15
15
|
label: string
|
|
16
16
|
/** Makes the row pressable and adds a chevron on the trailing edge. */
|
|
17
17
|
onPress?: () => void
|
|
18
|
-
/** Horizontal padding inside the row
|
|
18
|
+
/** Horizontal padding inside the row, and inside its press area too, so Android's ripple reaches the card's edges. */
|
|
19
19
|
px?: SpaceSize
|
|
20
20
|
/** Test identifier, forwarded to the native view. */
|
|
21
21
|
testID?: string
|