@hzblj/kara-components 0.6.0 → 0.7.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 +49 -0
- package/README.md +31 -3
- package/lib/module/atoms/checkbox/checkbox.android.js +7 -3
- package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
- package/lib/module/atoms/date-picker/date-picker.ios.js +32 -11
- package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
- package/lib/module/atoms/index.js +0 -1
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/atoms/rn-flat-list/rn-flat-list.js +10 -9
- package/lib/module/atoms/rn-flat-list/rn-flat-list.js.map +1 -1
- package/lib/module/atoms/rn-section-list/rn-section-list.js +5 -8
- package/lib/module/atoms/rn-section-list/rn-section-list.js.map +1 -1
- package/lib/module/atoms/segmented-control/index.js +1 -1
- package/lib/module/atoms/segmented-control/index.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.android.js +74 -0
- package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -0
- package/lib/module/atoms/segmented-control/segmented-control.ios.js +44 -0
- package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -0
- package/lib/module/atoms/switch/switch.android.js +4 -1
- package/lib/module/atoms/switch/switch.android.js.map +1 -1
- package/lib/module/molecules/search-bar/search-bar.js +15 -2
- package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
- package/lib/module/molecules/tooltip/tooltip.android.js +16 -5
- package/lib/module/molecules/tooltip/tooltip.android.js.map +1 -1
- package/lib/module/organisms/action-sheet/action-sheet.android.js +41 -20
- package/lib/module/organisms/action-sheet/action-sheet.android.js.map +1 -1
- package/lib/module/organisms/action-sheet/action-sheet.ios.js +24 -8
- package/lib/module/organisms/action-sheet/action-sheet.ios.js.map +1 -1
- package/lib/module/organisms/alert/alert.ios.js +15 -2
- package/lib/module/organisms/alert/alert.ios.js.map +1 -1
- package/lib/module/organisms/form/form.android.js +22 -9
- package/lib/module/organisms/form/form.android.js.map +1 -1
- package/lib/module/organisms/swipe-actions/swipe-actions.android.js +125 -11
- package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
- package/lib/module/organisms/swipe-actions/swipe-actions.ios.js +46 -18
- package/lib/module/organisms/swipe-actions/swipe-actions.ios.js.map +1 -1
- package/lib/module/templates/screen/screen.android.js +31 -10
- package/lib/module/templates/screen/screen.android.js.map +1 -1
- package/lib/typescript/atoms/avatar/avatar.types.d.ts +5 -0
- package/lib/typescript/atoms/badge/badge.types.d.ts +3 -0
- package/lib/typescript/atoms/button/button.types.d.ts +12 -0
- package/lib/typescript/atoms/card/card.types.d.ts +6 -0
- package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +5 -0
- package/lib/typescript/atoms/chip/chip.types.d.ts +5 -0
- package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +4 -0
- package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +6 -1
- package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +14 -0
- package/lib/typescript/atoms/divider/divider.types.d.ts +3 -0
- package/lib/typescript/atoms/fab/fab.types.d.ts +7 -0
- package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +1 -0
- package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +6 -0
- package/lib/typescript/atoms/icon/icon.types.d.ts +8 -0
- package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +5 -0
- package/lib/typescript/atoms/index.d.ts +0 -1
- package/lib/typescript/atoms/island/island.types.d.ts +4 -0
- package/lib/typescript/atoms/list/list.types.d.ts +2 -0
- package/lib/typescript/atoms/picker/picker.types.d.ts +12 -0
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +8 -0
- package/lib/typescript/atoms/progress/progress.types.d.ts +6 -0
- package/lib/typescript/atoms/rn-flat-list/rn-flat-list.d.ts +5 -8
- package/lib/typescript/atoms/rn-flat-list/rn-flat-list.types.d.ts +3 -1
- package/lib/typescript/atoms/rn-scroll-view/rn-scroll-view.types.d.ts +4 -0
- package/lib/typescript/atoms/rn-section-list/rn-section-list.d.ts +2 -8
- package/lib/typescript/atoms/rn-section-list/rn-section-list.types.d.ts +3 -1
- package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +4 -0
- package/lib/typescript/atoms/rn-view/rn-view.types.d.ts +2 -0
- package/lib/typescript/atoms/scroll-view/scroll-view.types.d.ts +4 -0
- package/lib/typescript/atoms/segmented-control/segmented-control.d.ts +1 -6
- package/lib/typescript/atoms/segmented-control/segmented-control.ios.d.ts +7 -0
- package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +13 -0
- package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +5 -0
- package/lib/typescript/atoms/slider/slider.types.d.ts +7 -0
- package/lib/typescript/atoms/spacer/spacer.types.d.ts +3 -0
- package/lib/typescript/atoms/surface/surface.types.d.ts +7 -0
- package/lib/typescript/atoms/switch/switch.types.d.ts +8 -0
- package/lib/typescript/atoms/text/text.types.d.ts +12 -0
- package/lib/typescript/atoms/text-field/text-field.types.d.ts +12 -0
- package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +9 -0
- package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +8 -0
- package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +8 -0
- package/lib/typescript/molecules/empty-state/empty-state.types.d.ts +5 -0
- package/lib/typescript/molecules/label/label.types.d.ts +6 -0
- package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +6 -0
- package/lib/typescript/molecules/link/link.types.d.ts +6 -0
- package/lib/typescript/molecules/list-item/list-item.types.d.ts +9 -0
- package/lib/typescript/molecules/navigation-bar/navigation-bar.types.d.ts +8 -0
- package/lib/typescript/molecules/pager/pager.types.d.ts +2 -0
- package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +10 -0
- 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 +11 -0
- package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +3 -0
- package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +8 -0
- package/lib/typescript/molecules/stepper/stepper.types.d.ts +13 -0
- package/lib/typescript/molecules/tooltip/tooltip.types.d.ts +12 -0
- package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +8 -0
- package/lib/typescript/organisms/alert/alert.types.d.ts +11 -0
- package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +4 -0
- package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +2 -0
- package/lib/typescript/organisms/form/form.types.d.ts +6 -0
- package/lib/typescript/organisms/menu/menu.types.d.ts +17 -0
- package/lib/typescript/organisms/sheet/sheet.types.d.ts +19 -0
- package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +8 -0
- package/lib/typescript/organisms/swipe-actions/swipe-actions.types.d.ts +36 -13
- package/lib/typescript/templates/screen/screen.types.d.ts +9 -0
- package/package.json +5 -7
- package/src/atoms/avatar/avatar.types.ts +5 -0
- package/src/atoms/badge/badge.types.ts +3 -0
- package/src/atoms/button/button.types.ts +12 -0
- package/src/atoms/card/card.types.ts +6 -0
- package/src/atoms/checkbox/checkbox.android.tsx +7 -3
- package/src/atoms/checkbox/checkbox.types.ts +5 -0
- package/src/atoms/chip/chip.types.ts +5 -0
- package/src/atoms/color-picker/color-picker.types.ts +4 -0
- package/src/atoms/date-picker/date-picker.ios.tsx +33 -13
- package/src/atoms/date-picker/date-picker.types.ts +14 -0
- package/src/atoms/divider/divider.types.ts +3 -0
- package/src/atoms/fab/fab.types.ts +7 -0
- package/src/atoms/glass-view/glass-view.types.ts +1 -0
- package/src/atoms/h-stack/h-stack.types.ts +6 -0
- package/src/atoms/icon/icon.types.ts +8 -0
- package/src/atoms/icon-button/icon-button.types.ts +5 -0
- package/src/atoms/index.ts +0 -1
- package/src/atoms/island/island.types.ts +4 -0
- package/src/atoms/list/list.types.ts +2 -0
- package/src/atoms/picker/picker.types.ts +12 -0
- package/src/atoms/pressable/pressable.types.ts +8 -0
- package/src/atoms/progress/progress.types.ts +6 -0
- package/src/atoms/rn-flat-list/rn-flat-list.tsx +8 -8
- package/src/atoms/rn-flat-list/rn-flat-list.types.ts +3 -1
- package/src/atoms/rn-scroll-view/rn-scroll-view.types.ts +4 -0
- package/src/atoms/rn-section-list/rn-section-list.tsx +5 -8
- package/src/atoms/rn-section-list/rn-section-list.types.ts +3 -1
- package/src/atoms/rn-text/rn-text.types.ts +4 -0
- package/src/atoms/rn-view/rn-view.types.ts +2 -0
- package/src/atoms/scroll-view/scroll-view.types.ts +4 -0
- package/src/atoms/segmented-control/segmented-control.android.tsx +79 -0
- package/src/atoms/segmented-control/segmented-control.ios.tsx +47 -0
- package/src/atoms/segmented-control/segmented-control.types.ts +13 -0
- package/src/atoms/skeleton/skeleton.types.ts +5 -0
- package/src/atoms/slider/slider.types.ts +7 -0
- package/src/atoms/spacer/spacer.types.ts +3 -0
- package/src/atoms/surface/surface.types.ts +7 -0
- package/src/atoms/switch/switch.android.tsx +4 -1
- package/src/atoms/switch/switch.types.ts +8 -0
- package/src/atoms/text/text.types.ts +12 -0
- package/src/atoms/text-field/text-field.types.ts +12 -0
- package/src/atoms/v-stack/v-stack.types.ts +9 -0
- package/src/atoms/z-stack/z-stack.types.ts +8 -0
- package/src/molecules/collapsible/collapsible.types.ts +8 -0
- package/src/molecules/empty-state/empty-state.types.ts +5 -0
- package/src/molecules/label/label.types.ts +6 -0
- package/src/molecules/labeled-content/labeled-content.types.ts +6 -0
- package/src/molecules/link/link.types.ts +6 -0
- package/src/molecules/list-item/list-item.types.ts +9 -0
- package/src/molecules/navigation-bar/navigation-bar.types.ts +8 -0
- package/src/molecules/pager/pager.types.ts +2 -0
- package/src/molecules/radio-group/radio-group.types.ts +10 -0
- package/src/molecules/rn-list-item/rn-list-item.types.ts +1 -1
- package/src/molecules/rn-tooltip/rn-tooltip.types.ts +11 -0
- package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +3 -0
- package/src/molecules/search-bar/search-bar.tsx +9 -2
- package/src/molecules/search-bar/search-bar.types.ts +8 -0
- package/src/molecules/stepper/stepper.types.ts +13 -0
- package/src/molecules/tooltip/tooltip.android.tsx +17 -9
- package/src/molecules/tooltip/tooltip.types.ts +12 -0
- package/src/organisms/action-sheet/action-sheet.android.tsx +37 -19
- package/src/organisms/action-sheet/action-sheet.ios.tsx +22 -9
- package/src/organisms/action-sheet/action-sheet.types.ts +8 -0
- package/src/organisms/alert/alert.ios.tsx +11 -1
- package/src/organisms/alert/alert.types.ts +11 -0
- package/src/organisms/context-menu/context-menu.types.ts +4 -0
- package/src/organisms/fab-menu/fab-menu.types.ts +2 -0
- package/src/organisms/form/form.android.tsx +15 -7
- package/src/organisms/form/form.types.ts +6 -0
- package/src/organisms/menu/menu.types.ts +17 -0
- package/src/organisms/sheet/sheet.types.ts +19 -0
- package/src/organisms/snackbar/snackbar.types.ts +8 -0
- package/src/organisms/swipe-actions/swipe-actions.android.tsx +129 -9
- package/src/organisms/swipe-actions/swipe-actions.ios.tsx +45 -11
- package/src/organisms/swipe-actions/swipe-actions.types.ts +37 -14
- package/src/templates/screen/screen.android.tsx +33 -12
- package/src/templates/screen/screen.types.ts +9 -0
- package/lib/module/atoms/flash-list/flash-list.js +0 -31
- package/lib/module/atoms/flash-list/flash-list.js.map +0 -1
- package/lib/module/atoms/flash-list/flash-list.types.js +0 -4
- package/lib/module/atoms/flash-list/flash-list.types.js.map +0 -1
- package/lib/module/atoms/flash-list/index.js +0 -5
- package/lib/module/atoms/flash-list/index.js.map +0 -1
- package/lib/module/atoms/segmented-control/segmented-control.js +0 -40
- package/lib/module/atoms/segmented-control/segmented-control.js.map +0 -1
- package/lib/typescript/atoms/flash-list/flash-list.d.ts +0 -8
- package/lib/typescript/atoms/flash-list/flash-list.types.d.ts +0 -18
- package/lib/typescript/atoms/flash-list/index.d.ts +0 -2
- package/src/atoms/flash-list/flash-list.tsx +0 -28
- package/src/atoms/flash-list/flash-list.types.ts +0 -20
- package/src/atoms/flash-list/index.ts +0 -2
- package/src/atoms/segmented-control/segmented-control.tsx +0 -47
|
@@ -1,14 +1,134 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import {FilledIconButton} from '@expo/ui/jetpack-compose'
|
|
2
|
+
import {size} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
|
+
import {SwipeActionsBox, type SwipeActionsBoxEdge} from '@hzblj/kara-native/jetpack-compose'
|
|
4
|
+
import {spacing, useAppTheme} from '@hzblj/kara-theme'
|
|
5
|
+
import {useCallback, useEffect, useId, useState, useSyncExternalStore} from 'react'
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
import {HStack} from '../../atoms/h-stack'
|
|
8
|
+
import {Icon} from '../../atoms/icon'
|
|
9
|
+
import {Text} from '../../atoms/text'
|
|
10
|
+
import {VStack} from '../../atoms/v-stack'
|
|
11
|
+
import {useHostGuard} from '../../host-context'
|
|
12
|
+
import type {SwipeAction, SwipeActionsComponent, SwipeActionsProps} from './swipe-actions.types'
|
|
7
13
|
|
|
8
|
-
|
|
14
|
+
/** A Material 3 Expressive wide icon button: wider than tall, a stadium, the label under it. */
|
|
15
|
+
const BUTTON = [size(52, 32)]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Which row is open, app-wide — SwiftUI's list keeps one row open, and so does this: opening one closes the last.
|
|
19
|
+
* React state cannot be shared between rows that know nothing of each other, so it lives in the module.
|
|
20
|
+
*/
|
|
21
|
+
let openRow: string | null = null
|
|
22
|
+
|
|
23
|
+
const listeners = new Set<() => void>()
|
|
24
|
+
|
|
25
|
+
const setOpenRow = (row: string | null) => {
|
|
26
|
+
if (openRow === row) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
openRow = row
|
|
31
|
+
|
|
32
|
+
for (const listener of listeners) {
|
|
33
|
+
listener()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const subscribe = (listener: () => void) => {
|
|
38
|
+
listeners.add(listener)
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
listeners.delete(listener)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getOpenRow = () => openRow
|
|
46
|
+
|
|
47
|
+
type ActionButtonProps = {action: SwipeAction; onDone: () => void}
|
|
48
|
+
|
|
49
|
+
const ActionButton = ({action, onDone}: ActionButtonProps) => {
|
|
50
|
+
const theme = useAppTheme()
|
|
51
|
+
const fill = action.tint ?? (action.destructive ? 'fill.destructive.primary' : 'fill.accent.primary')
|
|
52
|
+
const content = !action.tint && action.destructive ? 'content.on.destructive' : 'content.on.bg'
|
|
53
|
+
|
|
54
|
+
const onClick = useCallback(() => {
|
|
55
|
+
onDone()
|
|
56
|
+
action.onPress()
|
|
57
|
+
}, [action, onDone])
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<VStack align="center" spacing="s2">
|
|
61
|
+
<FilledIconButton
|
|
62
|
+
colors={{containerColor: theme.color(fill), contentColor: theme.color(content)}}
|
|
63
|
+
modifiers={BUTTON}
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
>
|
|
66
|
+
<Icon accessibilityLabel={action.label} color={content} name={action.icon} size="s20" />
|
|
67
|
+
</FilledIconButton>
|
|
68
|
+
<Text color="content.secondary" variant="labelMedium">
|
|
69
|
+
{action.label}
|
|
70
|
+
</Text>
|
|
71
|
+
</VStack>
|
|
72
|
+
)
|
|
9
73
|
}
|
|
10
74
|
|
|
11
|
-
|
|
12
|
-
|
|
75
|
+
const Actions = ({actions, onDone}: {actions: SwipeAction[]; onDone: () => void}) => (
|
|
76
|
+
<HStack align="center" spacing="s8">
|
|
77
|
+
{actions.map(action => (
|
|
78
|
+
<ActionButton action={action} key={action.label} onDone={onDone} />
|
|
79
|
+
))}
|
|
80
|
+
</HStack>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const close = () => setOpenRow(null)
|
|
13
84
|
|
|
14
|
-
|
|
85
|
+
/**
|
|
86
|
+
* kara's `SwipeActionsBox`: the row slides on an anchored drag and the buttons show in the strip it leaves, fading in.
|
|
87
|
+
* Controlled from here so one row is open at a time; a press closes the row before the action runs.
|
|
88
|
+
*/
|
|
89
|
+
export const SwipeActions: SwipeActionsComponent = ({children, leading = [], trailing = []}: SwipeActionsProps) => {
|
|
90
|
+
useHostGuard('SwipeActions')
|
|
91
|
+
const row = useId()
|
|
92
|
+
const isOpenRow = useSyncExternalStore(subscribe, getOpenRow) === row
|
|
93
|
+
const [edge, setEdge] = useState<SwipeActionsBoxEdge | null>(null)
|
|
94
|
+
|
|
95
|
+
const onOpenChange = useCallback(
|
|
96
|
+
(next: SwipeActionsBoxEdge | null) => {
|
|
97
|
+
setEdge(next)
|
|
98
|
+
|
|
99
|
+
if (next) {
|
|
100
|
+
setOpenRow(row)
|
|
101
|
+
} else if (openRow === row) {
|
|
102
|
+
setOpenRow(null)
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
[row]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// A row that goes away open must not keep the others from opening.
|
|
109
|
+
useEffect(
|
|
110
|
+
() => () => {
|
|
111
|
+
if (openRow === row) {
|
|
112
|
+
setOpenRow(null)
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
[row]
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<SwipeActionsBox actionsSpacing={spacing.s12} onOpenChange={onOpenChange} openEdge={isOpenRow ? edge : null}>
|
|
120
|
+
{leading.length > 0 ? (
|
|
121
|
+
<SwipeActionsBox.Leading>
|
|
122
|
+
<Actions actions={leading} onDone={close} />
|
|
123
|
+
</SwipeActionsBox.Leading>
|
|
124
|
+
) : null}
|
|
125
|
+
{trailing.length > 0 ? (
|
|
126
|
+
<SwipeActionsBox.Trailing>
|
|
127
|
+
{/* The first action sits outermost — at the right-hand end of a trailing row. */}
|
|
128
|
+
<Actions actions={[...trailing].reverse()} onDone={close} />
|
|
129
|
+
</SwipeActionsBox.Trailing>
|
|
130
|
+
) : null}
|
|
131
|
+
{children}
|
|
132
|
+
</SwipeActionsBox>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -1,18 +1,52 @@
|
|
|
1
|
-
import {SwipeActions as SwiftUISwipeActions} from '@expo/ui/swift-ui'
|
|
1
|
+
import {Button, Image, Label, SwipeActions as SwiftUISwipeActions} from '@expo/ui/swift-ui'
|
|
2
|
+
import {tint} from '@expo/ui/swift-ui/modifiers'
|
|
3
|
+
import {iconResourceName} from '@hzblj/kara-native'
|
|
4
|
+
import {useAppTheme} from '@hzblj/kara-theme'
|
|
2
5
|
|
|
3
6
|
import {useHostGuard} from '../../host-context'
|
|
4
|
-
import type {
|
|
7
|
+
import type {SwipeAction, SwipeActionsComponent, SwipeActionsProps} from './swipe-actions.types'
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
type ActionsProps = {actions: SwipeAction[]; allowsFullSwipe: boolean; edge: 'leading' | 'trailing'}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The edge's buttons, a `Label` each — iOS 26 draws its icon on a round button with the title under it; earlier
|
|
13
|
+
* versions, in a row of the usual height, the icon alone. No `destructive` role: SwiftUI would animate the row out by itself, and whether it goes is
|
|
14
|
+
* the list's data to say.
|
|
15
|
+
*/
|
|
16
|
+
const Actions = ({actions, allowsFullSwipe, edge}: ActionsProps) => {
|
|
17
|
+
const theme = useAppTheme()
|
|
8
18
|
|
|
9
|
-
return
|
|
19
|
+
return (
|
|
20
|
+
<SwiftUISwipeActions.Actions allowsFullSwipe={allowsFullSwipe} edge={edge}>
|
|
21
|
+
{actions.map(action => (
|
|
22
|
+
<Button
|
|
23
|
+
key={action.label}
|
|
24
|
+
modifiers={[
|
|
25
|
+
tint(theme.color(action.tint ?? (action.destructive ? 'fill.destructive.primary' : 'fill.accent.primary'))),
|
|
26
|
+
]}
|
|
27
|
+
onPress={action.onPress}
|
|
28
|
+
>
|
|
29
|
+
<Label icon={<Image assetName={iconResourceName(action.icon)} />} title={action.label} />
|
|
30
|
+
</Button>
|
|
31
|
+
))}
|
|
32
|
+
</SwiftUISwipeActions.Actions>
|
|
33
|
+
)
|
|
10
34
|
}
|
|
11
35
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
export const SwipeActions: SwipeActionsComponent = ({
|
|
37
|
+
allowsFullSwipe = true,
|
|
38
|
+
children,
|
|
39
|
+
leading = [],
|
|
40
|
+
testID,
|
|
41
|
+
trailing = [],
|
|
42
|
+
}: SwipeActionsProps) => {
|
|
43
|
+
useHostGuard('SwipeActions')
|
|
17
44
|
|
|
18
|
-
|
|
45
|
+
return (
|
|
46
|
+
<SwiftUISwipeActions testID={testID}>
|
|
47
|
+
{children}
|
|
48
|
+
{leading.length > 0 ? <Actions actions={leading} allowsFullSwipe={allowsFullSwipe} edge="leading" /> : null}
|
|
49
|
+
{trailing.length > 0 ? <Actions actions={trailing} allowsFullSwipe={allowsFullSwipe} edge="trailing" /> : null}
|
|
50
|
+
</SwiftUISwipeActions>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -1,24 +1,47 @@
|
|
|
1
|
+
import type {ColorToken} from '@hzblj/kara-theme'
|
|
1
2
|
import type {ReactNode} from 'react'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import type {IconName} from '../../atoms/icon'
|
|
5
|
+
|
|
6
|
+
/** One button a swiped row uncovers. */
|
|
7
|
+
export type SwipeAction = {
|
|
8
|
+
/** Fills the button with `fill.destructive.primary` unless `tint` says otherwise. */
|
|
9
|
+
destructive?: boolean
|
|
10
|
+
/** One of the app's icons, on the button — a round one on iOS 26, as in Mail, a filled stadium on Android. */
|
|
11
|
+
icon: IconName
|
|
12
|
+
/**
|
|
13
|
+
* The action's name, written under the button — before iOS 26 a row of the usual height shows the icon alone and
|
|
14
|
+
* keeps the label for VoiceOver. Actions are keyed by it, so keep labels unique within one row.
|
|
15
|
+
*/
|
|
16
|
+
label: string
|
|
17
|
+
/** Called when the button is pressed. The row closes by itself. */
|
|
18
|
+
onPress: () => void
|
|
19
|
+
/**
|
|
20
|
+
* The button's fill — `fill.destructive.primary` for a destructive action, `fill.accent.primary` otherwise. The icon
|
|
21
|
+
* on it is white, so pick a fill that holds white in both appearances.
|
|
22
|
+
*/
|
|
23
|
+
tint?: ColorToken
|
|
24
|
+
}
|
|
4
25
|
|
|
5
26
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
27
|
+
* A list row with buttons behind it, uncovered by a swipe — SwiftUI's `swipeActions` on iOS, and on Android, where
|
|
28
|
+
* Compose has none, kara's own: the row slides on an anchored drag and the buttons show in the strip it leaves. One
|
|
29
|
+
* row is open at a time on both; a tap on an open row closes it. A native organism — it belongs inside a `List`.
|
|
9
30
|
*/
|
|
10
31
|
export type SwipeActionsProps = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export type SwipeActionsGroupProps = {
|
|
16
|
-
/** Whether swiping all the way performs the first action in the group. */
|
|
32
|
+
/**
|
|
33
|
+
* iOS: a swipe all the way across performs the edge's first action, as in Mail. Android has no full swipe. `true`
|
|
34
|
+
* by default, as in SwiftUI.
|
|
35
|
+
*/
|
|
17
36
|
allowsFullSwipe?: boolean
|
|
37
|
+
/** The row. */
|
|
18
38
|
children: ReactNode
|
|
19
|
-
edge
|
|
39
|
+
/** The buttons at the leading edge, uncovered by a swipe toward the trailing one. The first sits outermost. */
|
|
40
|
+
leading?: SwipeAction[]
|
|
41
|
+
/** Test identifier. iOS: on the row. Android: the row is a Compose node, reach it by its content. */
|
|
42
|
+
testID?: string
|
|
43
|
+
/** The buttons at the trailing edge, uncovered by a swipe toward the leading one. The first sits outermost. */
|
|
44
|
+
trailing?: SwipeAction[]
|
|
20
45
|
}
|
|
21
46
|
|
|
22
|
-
export type SwipeActionsComponent = (
|
|
23
|
-
Actions: (props: SwipeActionsGroupProps) => ReactNode
|
|
24
|
-
}
|
|
47
|
+
export type SwipeActionsComponent = (props: SwipeActionsProps) => ReactNode
|
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
import {Column, Host} from '@expo/ui/jetpack-compose'
|
|
2
|
+
import {padding} from '@expo/ui/jetpack-compose/modifiers'
|
|
2
3
|
import {useSxModifiers} from '@hzblj/kara-sx'
|
|
3
4
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
-
import {
|
|
5
|
+
import {useCallback, useMemo, useRef, useState} from 'react'
|
|
6
|
+
import {StatusBar, StyleSheet, View} from 'react-native'
|
|
5
7
|
import {HostBoundary} from '../../host-context'
|
|
6
8
|
import type {ScreenComponent, ScreenProps} from './screen.types'
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The content keeps clear of as much of the status bar as the screen covers — all of it when nothing sits above the
|
|
12
|
+
* screen, none under a navigation bar — the way a SwiftUI host keeps to the safe area. Compose would draw under it:
|
|
13
|
+
* the app is edge to edge, and `Host` pads for nothing. The background still runs to the edge.
|
|
14
|
+
*/
|
|
8
15
|
export const Screen: ScreenComponent = ({bg = 'surface.base', children, ignoreKeyboardInset, testID}: ScreenProps) => {
|
|
9
16
|
const theme = useAppTheme()
|
|
10
|
-
const
|
|
17
|
+
const frame = useRef<View>(null)
|
|
18
|
+
const [statusBarOverlap, setStatusBarOverlap] = useState(0)
|
|
19
|
+
const sx = useSxModifiers({bg, tall: true, wide: true})
|
|
20
|
+
const modifiers = useMemo(
|
|
21
|
+
() => (statusBarOverlap > 0 ? [...sx, padding(0, statusBarOverlap, 0, 0)] : sx),
|
|
22
|
+
[statusBarOverlap, sx]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const onLayout = useCallback(() => {
|
|
26
|
+
frame.current?.measureInWindow((_x, y) => {
|
|
27
|
+
setStatusBarOverlap(Math.max(0, Math.round((StatusBar.currentHeight ?? 0) - y)))
|
|
28
|
+
})
|
|
29
|
+
}, [])
|
|
11
30
|
|
|
12
31
|
return (
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
<View onLayout={onLayout} ref={frame} style={StyleSheet.absoluteFill} testID={testID}>
|
|
33
|
+
<Host
|
|
34
|
+
colorScheme={theme.dark ? 'dark' : 'light'}
|
|
35
|
+
ignoreSafeAreaKeyboardInsets={ignoreKeyboardInset}
|
|
36
|
+
seedColor={theme.materialSeed}
|
|
37
|
+
style={StyleSheet.absoluteFill}
|
|
38
|
+
>
|
|
39
|
+
<Column modifiers={modifiers}>
|
|
40
|
+
<HostBoundary>{children}</HostBoundary>
|
|
41
|
+
</Column>
|
|
42
|
+
</Host>
|
|
43
|
+
</View>
|
|
23
44
|
)
|
|
24
45
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type {Sx} from '@hzblj/kara-sx'
|
|
2
2
|
import type {ReactNode} from 'react'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The full-bleed native host a screen starts with — a SwiftUI or Compose `Host` filling its parent, so native atoms
|
|
6
|
+
* render anywhere inside it. The background runs to the edges; the content keeps clear of the status bar where the
|
|
7
|
+
* screen covers it (no navigation bar above), on Android as SwiftUI's safe area does on iOS.
|
|
8
|
+
*/
|
|
4
9
|
export type ScreenProps = {
|
|
10
|
+
/** The screen's colour. `surface.base` by default. */
|
|
5
11
|
bg?: Sx['bg']
|
|
12
|
+
/** Laid out in a column that fills the screen. */
|
|
6
13
|
children?: ReactNode
|
|
14
|
+
/** Keeps the content where it is when the keyboard opens, instead of avoiding it. Android reads it once, on mount. */
|
|
7
15
|
ignoreKeyboardInset?: boolean
|
|
16
|
+
/** Test identifier — on the host view on iOS, on the view around it on Android. */
|
|
8
17
|
testID?: string
|
|
9
18
|
}
|
|
10
19
|
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { useSplitSx, useSx } from '@hzblj/kara-sx';
|
|
4
|
-
import { FlashList as ShopifyFlashList } from '@shopify/flash-list';
|
|
5
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
|
-
/** Stands in for an absent `content`, because a hook cannot be called conditionally. */
|
|
7
|
-
const NO_SX = {};
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The default list in this codebase: recycles its rows, so it holds a long list at a steady cost.
|
|
11
|
-
*
|
|
12
|
-
* `RnFlatList` and `RnSectionList` are the alternatives, and only for rows that host native views —
|
|
13
|
-
* see either one for why recycling and absolute positioning cannot carry an `@expo/ui` host.
|
|
14
|
-
*/
|
|
15
|
-
export const FlashList = ({
|
|
16
|
-
content,
|
|
17
|
-
data,
|
|
18
|
-
renderItem,
|
|
19
|
-
...props
|
|
20
|
-
}) => {
|
|
21
|
-
const [style, rest] = useSplitSx(props);
|
|
22
|
-
const contentStyle = useSx(content ?? NO_SX);
|
|
23
|
-
return /*#__PURE__*/_jsx(ShopifyFlashList, {
|
|
24
|
-
data: data,
|
|
25
|
-
renderItem: renderItem,
|
|
26
|
-
...rest,
|
|
27
|
-
contentContainerStyle: content === undefined ? undefined : contentStyle,
|
|
28
|
-
style: style
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
//# sourceMappingURL=flash-list.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["useSplitSx","useSx","FlashList","ShopifyFlashList","jsx","_jsx","NO_SX","content","data","renderItem","props","style","rest","contentStyle","contentContainerStyle","undefined"],"sourceRoot":"../../../../src","sources":["atoms/flash-list/flash-list.tsx"],"mappings":";;AAAA,SAAQA,UAAU,EAAEC,KAAK,QAAO,gBAAgB;AAChD,SAAQC,SAAS,IAAIC,gBAAgB,QAAO,qBAAqB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAIjE;AACA,MAAMC,KAAK,GAAG,CAAC,CAAC;;AAEhB;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMJ,SAAS,GAAGA,CAAS;EAACK,OAAO;EAAEC,IAAI;EAAEC,UAAU;EAAE,GAAGC;AAA4B,CAAC,KAAK;EACjG,MAAM,CAACC,KAAK,EAAEC,IAAI,CAAC,GAAGZ,UAAU,CAACU,KAAgC,CAAC;EAClE,MAAMG,YAAY,GAAGZ,KAAK,CAACM,OAAO,IAAID,KAAK,CAAC;EAE5C,oBACED,IAAA,CAACF,gBAAgB;IACfK,IAAI,EAAEA,IAAK;IACXC,UAAU,EAAEA,UAAW;IAAA,GACnBG,IAAI;IACRE,qBAAqB,EAAEP,OAAO,KAAKQ,SAAS,GAAGA,SAAS,GAAGF,YAAa;IACxEF,KAAK,EAAEA;EAAM,CACd,CAAC;AAEN,CAAC","ignoreList":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../../../src","sources":["atoms/flash-list/flash-list.types.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../../../src","sources":["atoms/flash-list/index.ts"],"mappings":";;AAAA,cAAc,iBAAc;AAC5B,cAAc,uBAAoB","ignoreList":[]}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { SegmentedControl as NativeSegmentedControl } from '@expo/ui/community/segmented-control';
|
|
4
|
-
import { useAppTheme } from '@hzblj/kara-theme';
|
|
5
|
-
import { useCallback } from 'react';
|
|
6
|
-
import { Platform } from 'react-native';
|
|
7
|
-
import { haptics } from "../../haptics/index.js";
|
|
8
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
-
/**
|
|
10
|
-
* `UISegmentedControl` on iOS, Material's single-choice segmented buttons on Android. A new segment plays `selection`
|
|
11
|
-
* on Android, whose buttons play nothing of their own; iOS 26's control already gives its own feedback.
|
|
12
|
-
*/
|
|
13
|
-
export const SegmentedControl = ({
|
|
14
|
-
disabled = false,
|
|
15
|
-
onChange,
|
|
16
|
-
selectedIndex,
|
|
17
|
-
testID,
|
|
18
|
-
values
|
|
19
|
-
}) => {
|
|
20
|
-
const theme = useAppTheme();
|
|
21
|
-
const onNativeChange = useCallback(event => {
|
|
22
|
-
const index = event.nativeEvent.selectedSegmentIndex;
|
|
23
|
-
|
|
24
|
-
// Material reports a tap on the segment that is already selected too.
|
|
25
|
-
if (Platform.OS === 'android' && index !== selectedIndex) {
|
|
26
|
-
haptics.selection();
|
|
27
|
-
}
|
|
28
|
-
onChange?.(index, event.nativeEvent.value);
|
|
29
|
-
}, [onChange, selectedIndex]);
|
|
30
|
-
return /*#__PURE__*/_jsx(NativeSegmentedControl, {
|
|
31
|
-
appearance: theme.dark ? 'dark' : 'light',
|
|
32
|
-
enabled: !disabled,
|
|
33
|
-
onChange: onNativeChange,
|
|
34
|
-
selectedIndex: selectedIndex,
|
|
35
|
-
testID: testID,
|
|
36
|
-
tintColor: theme.colorString('surface.primary'),
|
|
37
|
-
values: values
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
//# sourceMappingURL=segmented-control.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["SegmentedControl","NativeSegmentedControl","useAppTheme","useCallback","Platform","haptics","jsx","_jsx","disabled","onChange","selectedIndex","testID","values","theme","onNativeChange","event","index","nativeEvent","selectedSegmentIndex","OS","selection","value","appearance","dark","enabled","tintColor","colorString"],"sourceRoot":"../../../../src","sources":["atoms/segmented-control/segmented-control.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,IAAIC,sBAAsB,QAAO,sCAAsC;AAC/F,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,QAAO,OAAO;AACjC,SAAQC,QAAQ,QAAO,cAAc;AAErC,SAAQC,OAAO,QAAO,wBAAe;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAGrC;AACA;AACA;AACA;AACA,OAAO,MAAMP,gBAA2C,GAAGA,CAAC;EAC1DQ,QAAQ,GAAG,KAAK;EAChBC,QAAQ;EACRC,aAAa;EACbC,MAAM;EACNC;AACqB,CAAC,KAAK;EAC3B,MAAMC,KAAK,GAAGX,WAAW,CAAC,CAAC;EAE3B,MAAMY,cAAc,GAAGX,WAAW,CAC/BY,KAAmE,IAAK;IACvE,MAAMC,KAAK,GAAGD,KAAK,CAACE,WAAW,CAACC,oBAAoB;;IAEpD;IACA,IAAId,QAAQ,CAACe,EAAE,KAAK,SAAS,IAAIH,KAAK,KAAKN,aAAa,EAAE;MACxDL,OAAO,CAACe,SAAS,CAAC,CAAC;IACrB;IAEAX,QAAQ,GAAGO,KAAK,EAAED,KAAK,CAACE,WAAW,CAACI,KAAK,CAAC;EAC5C,CAAC,EACD,CAACZ,QAAQ,EAAEC,aAAa,CAC1B,CAAC;EAED,oBACEH,IAAA,CAACN,sBAAsB;IACrBqB,UAAU,EAAET,KAAK,CAACU,IAAI,GAAG,MAAM,GAAG,OAAQ;IAC1CC,OAAO,EAAE,CAAChB,QAAS;IACnBC,QAAQ,EAAEK,cAAe;IACzBJ,aAAa,EAAEA,aAAc;IAC7BC,MAAM,EAAEA,MAAO;IACfc,SAAS,EAAEZ,KAAK,CAACa,WAAW,CAAC,iBAAiB,CAAE;IAChDd,MAAM,EAAEA;EAAO,CAChB,CAAC;AAEN,CAAC","ignoreList":[]}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { FlashListProps } from './flash-list.types';
|
|
2
|
-
/**
|
|
3
|
-
* The default list in this codebase: recycles its rows, so it holds a long list at a steady cost.
|
|
4
|
-
*
|
|
5
|
-
* `RnFlatList` and `RnSectionList` are the alternatives, and only for rows that host native views —
|
|
6
|
-
* see either one for why recycling and absolute positioning cannot carry an `@expo/ui` host.
|
|
7
|
-
*/
|
|
8
|
-
export declare const FlashList: <TItem>({ content, data, renderItem, ...props }: FlashListProps<TItem>) => import("react").JSX.Element;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { ResponsiveViewSx } from '@hzblj/kara-sx';
|
|
2
|
-
import type { FlashListRef, FlashListProps as ShopifyFlashListProps } from '@shopify/flash-list';
|
|
3
|
-
import type { Ref } from 'react';
|
|
4
|
-
/**
|
|
5
|
-
* Top-level `sx` styles the viewport; `content` styles the scrolling content container — the same split
|
|
6
|
-
* as `RnScrollView` and the React Native lists.
|
|
7
|
-
*
|
|
8
|
-
* Padding belongs in `content` here and nowhere else. FlashList measures its rows against its own
|
|
9
|
-
* `style`, and padding there throws that measurement off; its docs put it as "margin is okay but padding
|
|
10
|
-
* is not". `content` has no such limit, because it lands on the scroll view's content container.
|
|
11
|
-
*
|
|
12
|
-
* `ref` is declared because this is a wrapper rather than the re-export it used to be — React 19 passes
|
|
13
|
-
* it as a plain prop, and a `FlashListRef` is how a caller reaches `scrollToIndex` and friends.
|
|
14
|
-
*/
|
|
15
|
-
export type FlashListProps<TItem> = ResponsiveViewSx & Omit<ShopifyFlashListProps<TItem>, 'contentContainerStyle' | 'style'> & {
|
|
16
|
-
content?: ResponsiveViewSx;
|
|
17
|
-
ref?: Ref<FlashListRef<TItem>>;
|
|
18
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import {useSplitSx, useSx} from '@hzblj/kara-sx'
|
|
2
|
-
import {FlashList as ShopifyFlashList} from '@shopify/flash-list'
|
|
3
|
-
|
|
4
|
-
import type {FlashListProps} from './flash-list.types'
|
|
5
|
-
|
|
6
|
-
/** Stands in for an absent `content`, because a hook cannot be called conditionally. */
|
|
7
|
-
const NO_SX = {}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The default list in this codebase: recycles its rows, so it holds a long list at a steady cost.
|
|
11
|
-
*
|
|
12
|
-
* `RnFlatList` and `RnSectionList` are the alternatives, and only for rows that host native views —
|
|
13
|
-
* see either one for why recycling and absolute positioning cannot carry an `@expo/ui` host.
|
|
14
|
-
*/
|
|
15
|
-
export const FlashList = <TItem,>({content, data, renderItem, ...props}: FlashListProps<TItem>) => {
|
|
16
|
-
const [style, rest] = useSplitSx(props as Record<string, unknown>)
|
|
17
|
-
const contentStyle = useSx(content ?? NO_SX)
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<ShopifyFlashList
|
|
21
|
-
data={data}
|
|
22
|
-
renderItem={renderItem}
|
|
23
|
-
{...rest}
|
|
24
|
-
contentContainerStyle={content === undefined ? undefined : contentStyle}
|
|
25
|
-
style={style}
|
|
26
|
-
/>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
2
|
-
import type {FlashListRef, FlashListProps as ShopifyFlashListProps} from '@shopify/flash-list'
|
|
3
|
-
import type {Ref} from 'react'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Top-level `sx` styles the viewport; `content` styles the scrolling content container — the same split
|
|
7
|
-
* as `RnScrollView` and the React Native lists.
|
|
8
|
-
*
|
|
9
|
-
* Padding belongs in `content` here and nowhere else. FlashList measures its rows against its own
|
|
10
|
-
* `style`, and padding there throws that measurement off; its docs put it as "margin is okay but padding
|
|
11
|
-
* is not". `content` has no such limit, because it lands on the scroll view's content container.
|
|
12
|
-
*
|
|
13
|
-
* `ref` is declared because this is a wrapper rather than the re-export it used to be — React 19 passes
|
|
14
|
-
* it as a plain prop, and a `FlashListRef` is how a caller reaches `scrollToIndex` and friends.
|
|
15
|
-
*/
|
|
16
|
-
export type FlashListProps<TItem> = ResponsiveViewSx &
|
|
17
|
-
Omit<ShopifyFlashListProps<TItem>, 'contentContainerStyle' | 'style'> & {
|
|
18
|
-
content?: ResponsiveViewSx
|
|
19
|
-
ref?: Ref<FlashListRef<TItem>>
|
|
20
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import {SegmentedControl as NativeSegmentedControl} from '@expo/ui/community/segmented-control'
|
|
2
|
-
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
|
-
import {useCallback} from 'react'
|
|
4
|
-
import {Platform} from 'react-native'
|
|
5
|
-
|
|
6
|
-
import {haptics} from '../../haptics'
|
|
7
|
-
import type {SegmentedControlComponent, SegmentedControlProps} from './segmented-control.types'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* `UISegmentedControl` on iOS, Material's single-choice segmented buttons on Android. A new segment plays `selection`
|
|
11
|
-
* on Android, whose buttons play nothing of their own; iOS 26's control already gives its own feedback.
|
|
12
|
-
*/
|
|
13
|
-
export const SegmentedControl: SegmentedControlComponent = ({
|
|
14
|
-
disabled = false,
|
|
15
|
-
onChange,
|
|
16
|
-
selectedIndex,
|
|
17
|
-
testID,
|
|
18
|
-
values,
|
|
19
|
-
}: SegmentedControlProps) => {
|
|
20
|
-
const theme = useAppTheme()
|
|
21
|
-
|
|
22
|
-
const onNativeChange = useCallback(
|
|
23
|
-
(event: {nativeEvent: {selectedSegmentIndex: number; value: string}}) => {
|
|
24
|
-
const index = event.nativeEvent.selectedSegmentIndex
|
|
25
|
-
|
|
26
|
-
// Material reports a tap on the segment that is already selected too.
|
|
27
|
-
if (Platform.OS === 'android' && index !== selectedIndex) {
|
|
28
|
-
haptics.selection()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
onChange?.(index, event.nativeEvent.value)
|
|
32
|
-
},
|
|
33
|
-
[onChange, selectedIndex]
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<NativeSegmentedControl
|
|
38
|
-
appearance={theme.dark ? 'dark' : 'light'}
|
|
39
|
-
enabled={!disabled}
|
|
40
|
-
onChange={onNativeChange}
|
|
41
|
-
selectedIndex={selectedIndex}
|
|
42
|
-
testID={testID}
|
|
43
|
-
tintColor={theme.colorString('surface.primary')}
|
|
44
|
-
values={values}
|
|
45
|
-
/>
|
|
46
|
-
)
|
|
47
|
-
}
|