@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +31 -3
  3. package/lib/module/atoms/checkbox/checkbox.android.js +7 -3
  4. package/lib/module/atoms/checkbox/checkbox.android.js.map +1 -1
  5. package/lib/module/atoms/date-picker/date-picker.ios.js +32 -11
  6. package/lib/module/atoms/date-picker/date-picker.ios.js.map +1 -1
  7. package/lib/module/atoms/index.js +0 -1
  8. package/lib/module/atoms/index.js.map +1 -1
  9. package/lib/module/atoms/rn-flat-list/rn-flat-list.js +10 -9
  10. package/lib/module/atoms/rn-flat-list/rn-flat-list.js.map +1 -1
  11. package/lib/module/atoms/rn-section-list/rn-section-list.js +5 -8
  12. package/lib/module/atoms/rn-section-list/rn-section-list.js.map +1 -1
  13. package/lib/module/atoms/segmented-control/index.js +1 -1
  14. package/lib/module/atoms/segmented-control/index.js.map +1 -1
  15. package/lib/module/atoms/segmented-control/segmented-control.android.js +74 -0
  16. package/lib/module/atoms/segmented-control/segmented-control.android.js.map +1 -0
  17. package/lib/module/atoms/segmented-control/segmented-control.ios.js +44 -0
  18. package/lib/module/atoms/segmented-control/segmented-control.ios.js.map +1 -0
  19. package/lib/module/atoms/switch/switch.android.js +4 -1
  20. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  21. package/lib/module/molecules/search-bar/search-bar.js +15 -2
  22. package/lib/module/molecules/search-bar/search-bar.js.map +1 -1
  23. package/lib/module/molecules/tooltip/tooltip.android.js +16 -5
  24. package/lib/module/molecules/tooltip/tooltip.android.js.map +1 -1
  25. package/lib/module/organisms/action-sheet/action-sheet.android.js +41 -20
  26. package/lib/module/organisms/action-sheet/action-sheet.android.js.map +1 -1
  27. package/lib/module/organisms/action-sheet/action-sheet.ios.js +24 -8
  28. package/lib/module/organisms/action-sheet/action-sheet.ios.js.map +1 -1
  29. package/lib/module/organisms/alert/alert.ios.js +15 -2
  30. package/lib/module/organisms/alert/alert.ios.js.map +1 -1
  31. package/lib/module/organisms/form/form.android.js +22 -9
  32. package/lib/module/organisms/form/form.android.js.map +1 -1
  33. package/lib/module/organisms/swipe-actions/swipe-actions.android.js +125 -11
  34. package/lib/module/organisms/swipe-actions/swipe-actions.android.js.map +1 -1
  35. package/lib/module/organisms/swipe-actions/swipe-actions.ios.js +46 -18
  36. package/lib/module/organisms/swipe-actions/swipe-actions.ios.js.map +1 -1
  37. package/lib/module/templates/screen/screen.android.js +31 -10
  38. package/lib/module/templates/screen/screen.android.js.map +1 -1
  39. package/lib/typescript/atoms/avatar/avatar.types.d.ts +5 -0
  40. package/lib/typescript/atoms/badge/badge.types.d.ts +3 -0
  41. package/lib/typescript/atoms/button/button.types.d.ts +12 -0
  42. package/lib/typescript/atoms/card/card.types.d.ts +6 -0
  43. package/lib/typescript/atoms/checkbox/checkbox.types.d.ts +5 -0
  44. package/lib/typescript/atoms/chip/chip.types.d.ts +5 -0
  45. package/lib/typescript/atoms/color-picker/color-picker.types.d.ts +4 -0
  46. package/lib/typescript/atoms/date-picker/date-picker.ios.d.ts +6 -1
  47. package/lib/typescript/atoms/date-picker/date-picker.types.d.ts +14 -0
  48. package/lib/typescript/atoms/divider/divider.types.d.ts +3 -0
  49. package/lib/typescript/atoms/fab/fab.types.d.ts +7 -0
  50. package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +1 -0
  51. package/lib/typescript/atoms/h-stack/h-stack.types.d.ts +6 -0
  52. package/lib/typescript/atoms/icon/icon.types.d.ts +8 -0
  53. package/lib/typescript/atoms/icon-button/icon-button.types.d.ts +5 -0
  54. package/lib/typescript/atoms/index.d.ts +0 -1
  55. package/lib/typescript/atoms/island/island.types.d.ts +4 -0
  56. package/lib/typescript/atoms/list/list.types.d.ts +2 -0
  57. package/lib/typescript/atoms/picker/picker.types.d.ts +12 -0
  58. package/lib/typescript/atoms/pressable/pressable.types.d.ts +8 -0
  59. package/lib/typescript/atoms/progress/progress.types.d.ts +6 -0
  60. package/lib/typescript/atoms/rn-flat-list/rn-flat-list.d.ts +5 -8
  61. package/lib/typescript/atoms/rn-flat-list/rn-flat-list.types.d.ts +3 -1
  62. package/lib/typescript/atoms/rn-scroll-view/rn-scroll-view.types.d.ts +4 -0
  63. package/lib/typescript/atoms/rn-section-list/rn-section-list.d.ts +2 -8
  64. package/lib/typescript/atoms/rn-section-list/rn-section-list.types.d.ts +3 -1
  65. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +4 -0
  66. package/lib/typescript/atoms/rn-view/rn-view.types.d.ts +2 -0
  67. package/lib/typescript/atoms/scroll-view/scroll-view.types.d.ts +4 -0
  68. package/lib/typescript/atoms/segmented-control/segmented-control.d.ts +1 -6
  69. package/lib/typescript/atoms/segmented-control/segmented-control.ios.d.ts +7 -0
  70. package/lib/typescript/atoms/segmented-control/segmented-control.types.d.ts +13 -0
  71. package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +5 -0
  72. package/lib/typescript/atoms/slider/slider.types.d.ts +7 -0
  73. package/lib/typescript/atoms/spacer/spacer.types.d.ts +3 -0
  74. package/lib/typescript/atoms/surface/surface.types.d.ts +7 -0
  75. package/lib/typescript/atoms/switch/switch.types.d.ts +8 -0
  76. package/lib/typescript/atoms/text/text.types.d.ts +12 -0
  77. package/lib/typescript/atoms/text-field/text-field.types.d.ts +12 -0
  78. package/lib/typescript/atoms/v-stack/v-stack.types.d.ts +9 -0
  79. package/lib/typescript/atoms/z-stack/z-stack.types.d.ts +8 -0
  80. package/lib/typescript/molecules/collapsible/collapsible.types.d.ts +8 -0
  81. package/lib/typescript/molecules/empty-state/empty-state.types.d.ts +5 -0
  82. package/lib/typescript/molecules/label/label.types.d.ts +6 -0
  83. package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +6 -0
  84. package/lib/typescript/molecules/link/link.types.d.ts +6 -0
  85. package/lib/typescript/molecules/list-item/list-item.types.d.ts +9 -0
  86. package/lib/typescript/molecules/navigation-bar/navigation-bar.types.d.ts +8 -0
  87. package/lib/typescript/molecules/pager/pager.types.d.ts +2 -0
  88. package/lib/typescript/molecules/radio-group/radio-group.types.d.ts +10 -0
  89. package/lib/typescript/molecules/rn-list-item/rn-list-item.types.d.ts +1 -1
  90. package/lib/typescript/molecules/rn-tooltip/rn-tooltip.types.d.ts +11 -0
  91. package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +3 -0
  92. package/lib/typescript/molecules/search-bar/search-bar.types.d.ts +8 -0
  93. package/lib/typescript/molecules/stepper/stepper.types.d.ts +13 -0
  94. package/lib/typescript/molecules/tooltip/tooltip.types.d.ts +12 -0
  95. package/lib/typescript/organisms/action-sheet/action-sheet.types.d.ts +8 -0
  96. package/lib/typescript/organisms/alert/alert.types.d.ts +11 -0
  97. package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +4 -0
  98. package/lib/typescript/organisms/fab-menu/fab-menu.types.d.ts +2 -0
  99. package/lib/typescript/organisms/form/form.types.d.ts +6 -0
  100. package/lib/typescript/organisms/menu/menu.types.d.ts +17 -0
  101. package/lib/typescript/organisms/sheet/sheet.types.d.ts +19 -0
  102. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +8 -0
  103. package/lib/typescript/organisms/swipe-actions/swipe-actions.types.d.ts +36 -13
  104. package/lib/typescript/templates/screen/screen.types.d.ts +9 -0
  105. package/package.json +5 -7
  106. package/src/atoms/avatar/avatar.types.ts +5 -0
  107. package/src/atoms/badge/badge.types.ts +3 -0
  108. package/src/atoms/button/button.types.ts +12 -0
  109. package/src/atoms/card/card.types.ts +6 -0
  110. package/src/atoms/checkbox/checkbox.android.tsx +7 -3
  111. package/src/atoms/checkbox/checkbox.types.ts +5 -0
  112. package/src/atoms/chip/chip.types.ts +5 -0
  113. package/src/atoms/color-picker/color-picker.types.ts +4 -0
  114. package/src/atoms/date-picker/date-picker.ios.tsx +33 -13
  115. package/src/atoms/date-picker/date-picker.types.ts +14 -0
  116. package/src/atoms/divider/divider.types.ts +3 -0
  117. package/src/atoms/fab/fab.types.ts +7 -0
  118. package/src/atoms/glass-view/glass-view.types.ts +1 -0
  119. package/src/atoms/h-stack/h-stack.types.ts +6 -0
  120. package/src/atoms/icon/icon.types.ts +8 -0
  121. package/src/atoms/icon-button/icon-button.types.ts +5 -0
  122. package/src/atoms/index.ts +0 -1
  123. package/src/atoms/island/island.types.ts +4 -0
  124. package/src/atoms/list/list.types.ts +2 -0
  125. package/src/atoms/picker/picker.types.ts +12 -0
  126. package/src/atoms/pressable/pressable.types.ts +8 -0
  127. package/src/atoms/progress/progress.types.ts +6 -0
  128. package/src/atoms/rn-flat-list/rn-flat-list.tsx +8 -8
  129. package/src/atoms/rn-flat-list/rn-flat-list.types.ts +3 -1
  130. package/src/atoms/rn-scroll-view/rn-scroll-view.types.ts +4 -0
  131. package/src/atoms/rn-section-list/rn-section-list.tsx +5 -8
  132. package/src/atoms/rn-section-list/rn-section-list.types.ts +3 -1
  133. package/src/atoms/rn-text/rn-text.types.ts +4 -0
  134. package/src/atoms/rn-view/rn-view.types.ts +2 -0
  135. package/src/atoms/scroll-view/scroll-view.types.ts +4 -0
  136. package/src/atoms/segmented-control/segmented-control.android.tsx +79 -0
  137. package/src/atoms/segmented-control/segmented-control.ios.tsx +47 -0
  138. package/src/atoms/segmented-control/segmented-control.types.ts +13 -0
  139. package/src/atoms/skeleton/skeleton.types.ts +5 -0
  140. package/src/atoms/slider/slider.types.ts +7 -0
  141. package/src/atoms/spacer/spacer.types.ts +3 -0
  142. package/src/atoms/surface/surface.types.ts +7 -0
  143. package/src/atoms/switch/switch.android.tsx +4 -1
  144. package/src/atoms/switch/switch.types.ts +8 -0
  145. package/src/atoms/text/text.types.ts +12 -0
  146. package/src/atoms/text-field/text-field.types.ts +12 -0
  147. package/src/atoms/v-stack/v-stack.types.ts +9 -0
  148. package/src/atoms/z-stack/z-stack.types.ts +8 -0
  149. package/src/molecules/collapsible/collapsible.types.ts +8 -0
  150. package/src/molecules/empty-state/empty-state.types.ts +5 -0
  151. package/src/molecules/label/label.types.ts +6 -0
  152. package/src/molecules/labeled-content/labeled-content.types.ts +6 -0
  153. package/src/molecules/link/link.types.ts +6 -0
  154. package/src/molecules/list-item/list-item.types.ts +9 -0
  155. package/src/molecules/navigation-bar/navigation-bar.types.ts +8 -0
  156. package/src/molecules/pager/pager.types.ts +2 -0
  157. package/src/molecules/radio-group/radio-group.types.ts +10 -0
  158. package/src/molecules/rn-list-item/rn-list-item.types.ts +1 -1
  159. package/src/molecules/rn-tooltip/rn-tooltip.types.ts +11 -0
  160. package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +3 -0
  161. package/src/molecules/search-bar/search-bar.tsx +9 -2
  162. package/src/molecules/search-bar/search-bar.types.ts +8 -0
  163. package/src/molecules/stepper/stepper.types.ts +13 -0
  164. package/src/molecules/tooltip/tooltip.android.tsx +17 -9
  165. package/src/molecules/tooltip/tooltip.types.ts +12 -0
  166. package/src/organisms/action-sheet/action-sheet.android.tsx +37 -19
  167. package/src/organisms/action-sheet/action-sheet.ios.tsx +22 -9
  168. package/src/organisms/action-sheet/action-sheet.types.ts +8 -0
  169. package/src/organisms/alert/alert.ios.tsx +11 -1
  170. package/src/organisms/alert/alert.types.ts +11 -0
  171. package/src/organisms/context-menu/context-menu.types.ts +4 -0
  172. package/src/organisms/fab-menu/fab-menu.types.ts +2 -0
  173. package/src/organisms/form/form.android.tsx +15 -7
  174. package/src/organisms/form/form.types.ts +6 -0
  175. package/src/organisms/menu/menu.types.ts +17 -0
  176. package/src/organisms/sheet/sheet.types.ts +19 -0
  177. package/src/organisms/snackbar/snackbar.types.ts +8 -0
  178. package/src/organisms/swipe-actions/swipe-actions.android.tsx +129 -9
  179. package/src/organisms/swipe-actions/swipe-actions.ios.tsx +45 -11
  180. package/src/organisms/swipe-actions/swipe-actions.types.ts +37 -14
  181. package/src/templates/screen/screen.android.tsx +33 -12
  182. package/src/templates/screen/screen.types.ts +9 -0
  183. package/lib/module/atoms/flash-list/flash-list.js +0 -31
  184. package/lib/module/atoms/flash-list/flash-list.js.map +0 -1
  185. package/lib/module/atoms/flash-list/flash-list.types.js +0 -4
  186. package/lib/module/atoms/flash-list/flash-list.types.js.map +0 -1
  187. package/lib/module/atoms/flash-list/index.js +0 -5
  188. package/lib/module/atoms/flash-list/index.js.map +0 -1
  189. package/lib/module/atoms/segmented-control/segmented-control.js +0 -40
  190. package/lib/module/atoms/segmented-control/segmented-control.js.map +0 -1
  191. package/lib/typescript/atoms/flash-list/flash-list.d.ts +0 -8
  192. package/lib/typescript/atoms/flash-list/flash-list.types.d.ts +0 -18
  193. package/lib/typescript/atoms/flash-list/index.d.ts +0 -2
  194. package/src/atoms/flash-list/flash-list.tsx +0 -28
  195. package/src/atoms/flash-list/flash-list.types.ts +0 -20
  196. package/src/atoms/flash-list/index.ts +0 -2
  197. package/src/atoms/segmented-control/segmented-control.tsx +0 -47
@@ -1,14 +1,134 @@
1
- import {warnUnsupported} from '../../platform-support'
2
- import type {SwipeActionsComponent, SwipeActionsProps} from './swipe-actions.types'
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
- /** Passes the row straight through, so losing the gesture never costs you the content. */
5
- const SwipeActionsRoot = ({children}: SwipeActionsProps) => {
6
- warnUnsupported('SwipeActions', 'Android')
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
- return children
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
- /** The buttons belong to a gesture that does not exist here, so they are dropped. */
12
- const SwipeActionsGroup = () => null
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
- export const SwipeActions: SwipeActionsComponent = Object.assign(SwipeActionsRoot, {Actions: SwipeActionsGroup})
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 {SwipeActionsComponent, SwipeActionsGroupProps, SwipeActionsProps} from './swipe-actions.types'
7
+ import type {SwipeAction, SwipeActionsComponent, SwipeActionsProps} from './swipe-actions.types'
5
8
 
6
- const SwipeActionsRoot = ({children, testID}: SwipeActionsProps) => {
7
- useHostGuard('SwipeActions')
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 <SwiftUISwipeActions testID={testID}>{children}</SwiftUISwipeActions>
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 SwipeActionsGroup = ({allowsFullSwipe = true, children, edge = 'trailing'}: SwipeActionsGroupProps) => (
13
- <SwiftUISwipeActions.Actions allowsFullSwipe={allowsFullSwipe} edge={edge}>
14
- {children}
15
- </SwiftUISwipeActions.Actions>
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
- export const SwipeActions: SwipeActionsComponent = Object.assign(SwipeActionsRoot, {Actions: SwipeActionsGroup})
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
- export type SwipeActionsEdge = 'leading' | 'trailing'
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
- * Swipe-to-reveal buttons on a row. iOS only — Compose ships no equivalent, so on Android the
7
- * row renders exactly as it would without this wrapper and the action buttons are dropped.
8
- * Degrading that way rather than to `null` keeps the content on screen.
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
- children: ReactNode
12
- testID?: string
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?: SwipeActionsEdge
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 = ((props: SwipeActionsProps) => ReactNode) & {
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 {StyleSheet} from 'react-native'
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 modifiers = useSxModifiers({bg, tall: true, wide: true})
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
- <Host
14
- colorScheme={theme.dark ? 'dark' : 'light'}
15
- ignoreSafeAreaKeyboardInsets={ignoreKeyboardInset}
16
- seedColor={theme.materialSeed}
17
- style={StyleSheet.absoluteFill}
18
- >
19
- <Column modifiers={modifiers}>
20
- <HostBoundary>{children}</HostBoundary>
21
- </Column>
22
- </Host>
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,4 +0,0 @@
1
- "use strict";
2
-
3
- export {};
4
- //# sourceMappingURL=flash-list.types.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":[],"sourceRoot":"../../../../src","sources":["atoms/flash-list/flash-list.types.ts"],"mappings":"","ignoreList":[]}
@@ -1,5 +0,0 @@
1
- "use strict";
2
-
3
- export * from "./flash-list.js";
4
- export * from "./flash-list.types.js";
5
- //# sourceMappingURL=index.js.map
@@ -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,2 +0,0 @@
1
- export * from './flash-list';
2
- export * from './flash-list.types';
@@ -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,2 +0,0 @@
1
- export * from './flash-list'
2
- export * from './flash-list.types'
@@ -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
- }