@hzblj/kara-components 0.2.1 → 0.4.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 (92) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -3
  3. package/lib/module/atoms/h-stack/h-stack.android.js +2 -5
  4. package/lib/module/atoms/h-stack/h-stack.android.js.map +1 -1
  5. package/lib/module/atoms/v-stack/v-stack.android.js +2 -5
  6. package/lib/module/atoms/v-stack/v-stack.android.js.map +1 -1
  7. package/lib/module/atoms/z-stack/z-stack.android.js +2 -5
  8. package/lib/module/atoms/z-stack/z-stack.android.js.map +1 -1
  9. package/lib/module/molecules/index.js +1 -0
  10. package/lib/module/molecules/index.js.map +1 -1
  11. package/lib/module/molecules/labeled-content/labeled-content.js +2 -0
  12. package/lib/module/molecules/labeled-content/labeled-content.js.map +1 -1
  13. package/lib/module/molecules/list-item/list-item.js +12 -1
  14. package/lib/module/molecules/list-item/list-item.js.map +1 -1
  15. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +65 -35
  16. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  17. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +48 -13
  18. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
  19. package/lib/module/molecules/rn-list-item/index.js +5 -0
  20. package/lib/module/molecules/rn-list-item/index.js.map +1 -0
  21. package/lib/module/molecules/rn-list-item/rn-list-item.js +71 -0
  22. package/lib/module/molecules/rn-list-item/rn-list-item.js.map +1 -0
  23. package/lib/module/molecules/rn-list-item/rn-list-item.types.js +4 -0
  24. package/lib/module/molecules/rn-list-item/rn-list-item.types.js.map +1 -0
  25. package/lib/module/organisms/context-menu/context-menu.android.js +83 -38
  26. package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
  27. package/lib/module/organisms/sheet/index.js +2 -1
  28. package/lib/module/organisms/sheet/index.js.map +1 -1
  29. package/lib/module/organisms/sheet/sheet-header.js +7 -0
  30. package/lib/module/organisms/sheet/sheet-header.js.map +1 -0
  31. package/lib/module/organisms/sheet/sheet-scroll.js +62 -0
  32. package/lib/module/organisms/sheet/sheet-scroll.js.map +1 -0
  33. package/lib/module/organisms/sheet/sheet.js +136 -0
  34. package/lib/module/organisms/sheet/sheet.js.map +1 -0
  35. package/lib/module/organisms/sheet/split-sheet-children.js +21 -0
  36. package/lib/module/organisms/sheet/split-sheet-children.js.map +1 -0
  37. package/lib/module/organisms/sheet/use-sheet-header.js +118 -0
  38. package/lib/module/organisms/sheet/use-sheet-header.js.map +1 -0
  39. package/lib/module/ripple-clip.js +36 -0
  40. package/lib/module/ripple-clip.js.map +1 -0
  41. package/lib/module/use-stack-modifiers.js +35 -0
  42. package/lib/module/use-stack-modifiers.js.map +1 -0
  43. package/lib/typescript/molecules/index.d.ts +1 -0
  44. package/lib/typescript/molecules/labeled-content/labeled-content.types.d.ts +3 -0
  45. package/lib/typescript/molecules/list-item/list-item.types.d.ts +6 -0
  46. package/lib/typescript/molecules/navigation-bar/navigation-bar.types.d.ts +56 -17
  47. package/lib/typescript/molecules/rn-list-item/index.d.ts +2 -0
  48. package/lib/typescript/molecules/rn-list-item/rn-list-item.d.ts +2 -0
  49. package/lib/typescript/molecules/rn-list-item/rn-list-item.types.d.ts +10 -0
  50. package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +2 -1
  51. package/lib/typescript/organisms/sheet/index.d.ts +1 -0
  52. package/lib/typescript/organisms/sheet/sheet-header.d.ts +3 -0
  53. package/lib/typescript/organisms/sheet/sheet-scroll.d.ts +9 -0
  54. package/lib/typescript/organisms/sheet/sheet.d.ts +7 -1
  55. package/lib/typescript/organisms/sheet/sheet.types.d.ts +86 -3
  56. package/lib/typescript/organisms/sheet/split-sheet-children.d.ts +9 -0
  57. package/lib/typescript/organisms/sheet/use-sheet-header.d.ts +24 -0
  58. package/lib/typescript/ripple-clip.d.ts +12 -0
  59. package/lib/typescript/use-stack-modifiers.d.ts +8 -0
  60. package/package.json +6 -4
  61. package/src/atoms/h-stack/h-stack.android.tsx +2 -8
  62. package/src/atoms/v-stack/v-stack.android.tsx +2 -8
  63. package/src/atoms/z-stack/z-stack.android.tsx +2 -8
  64. package/src/molecules/index.ts +1 -0
  65. package/src/molecules/labeled-content/labeled-content.tsx +2 -1
  66. package/src/molecules/labeled-content/labeled-content.types.ts +3 -0
  67. package/src/molecules/list-item/list-item.tsx +12 -4
  68. package/src/molecules/list-item/list-item.types.ts +6 -0
  69. package/src/molecules/navigation-bar/navigation-bar.android.tsx +50 -23
  70. package/src/molecules/navigation-bar/navigation-bar.ios.tsx +54 -15
  71. package/src/molecules/navigation-bar/navigation-bar.types.ts +56 -17
  72. package/src/molecules/rn-list-item/index.ts +2 -0
  73. package/src/molecules/rn-list-item/rn-list-item.tsx +48 -0
  74. package/src/molecules/rn-list-item/rn-list-item.types.ts +13 -0
  75. package/src/organisms/context-menu/context-menu.android.tsx +63 -38
  76. package/src/organisms/context-menu/context-menu.types.ts +2 -1
  77. package/src/organisms/sheet/index.ts +1 -0
  78. package/src/organisms/sheet/sheet-header.tsx +6 -0
  79. package/src/organisms/sheet/sheet-scroll.tsx +69 -0
  80. package/src/organisms/sheet/sheet.tsx +140 -0
  81. package/src/organisms/sheet/sheet.types.ts +93 -3
  82. package/src/organisms/sheet/split-sheet-children.ts +23 -0
  83. package/src/organisms/sheet/use-sheet-header.tsx +135 -0
  84. package/src/ripple-clip.ts +47 -0
  85. package/src/use-stack-modifiers.ts +24 -0
  86. package/lib/module/organisms/sheet/sheet.android.js +0 -26
  87. package/lib/module/organisms/sheet/sheet.android.js.map +0 -1
  88. package/lib/module/organisms/sheet/sheet.ios.js +0 -25
  89. package/lib/module/organisms/sheet/sheet.ios.js.map +0 -1
  90. package/lib/typescript/organisms/sheet/sheet.ios.d.ts +0 -2
  91. package/src/organisms/sheet/sheet.android.tsx +0 -25
  92. package/src/organisms/sheet/sheet.ios.tsx +0 -23
@@ -0,0 +1,140 @@
1
+ import {useAppTheme} from '@hzblj/kara-theme'
2
+ import {type DetentChangeEvent, TrueSheet} from '@lodev09/react-native-true-sheet'
3
+ import {useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef} from 'react'
4
+ import {Platform, StyleSheet, View} from 'react-native'
5
+ import type {SheetComponent, SheetDetent, SheetProps} from './sheet.types'
6
+ import {SheetHeader} from './sheet-header'
7
+ import {SheetScrollContext, SheetScrollView} from './sheet-scroll'
8
+ import {splitSheetChildren} from './split-sheet-children'
9
+ import {useSheetHeader} from './use-sheet-header'
10
+
11
+ const FIT_CONTENT: SheetDetent[] = ['auto']
12
+
13
+ /** Material's extra-large shape for a bottom sheet; iOS keeps the system's own, which follows the device's corners. */
14
+ const CORNER_RADIUS = Platform.OS === 'android' ? 28 : undefined
15
+
16
+ /** Keeps the sheet's own native view out of the layout it is declared in. */
17
+ const styles = StyleSheet.create({
18
+ host: {position: 'absolute'},
19
+ })
20
+
21
+ const SheetRoot = ({
22
+ bg = 'surface.primary',
23
+ children,
24
+ detents = FIT_CONTENT,
25
+ dimmed = true,
26
+ dismissible = true,
27
+ draggable = true,
28
+ footer,
29
+ grabber = true,
30
+ onDetentChange,
31
+ onDismiss,
32
+ onPresent,
33
+ ref,
34
+ scrollable = false,
35
+ testID,
36
+ visible,
37
+ }: SheetProps) => {
38
+ const theme = useAppTheme()
39
+ const {content, header: headerProps} = useMemo(() => splitSheetChildren(children), [children])
40
+ const sheetHeader = useSheetHeader({header: headerProps, scrollable})
41
+ const sheetRef = useRef<TrueSheet>(null)
42
+ const presented = useRef(false)
43
+ /** A dismissal we asked for: `onDismiss` reports the user's, never ours. */
44
+ const programmaticDismiss = useRef(false)
45
+
46
+ useImperativeHandle(
47
+ ref,
48
+ () => ({
49
+ dismiss: () => {
50
+ presented.current = false
51
+ programmaticDismiss.current = true
52
+ return sheetRef.current?.dismiss() ?? Promise.resolve()
53
+ },
54
+ present: detentIndex => {
55
+ presented.current = true
56
+ return sheetRef.current?.present(detentIndex) ?? Promise.resolve()
57
+ },
58
+ resize: detentIndex => sheetRef.current?.resize(detentIndex) ?? Promise.resolve(),
59
+ }),
60
+ []
61
+ )
62
+
63
+ useEffect(() => {
64
+ if (visible && !presented.current) {
65
+ presented.current = true
66
+ void sheetRef.current?.present()
67
+ } else if (!visible && presented.current) {
68
+ presented.current = false
69
+ programmaticDismiss.current = true
70
+ void sheetRef.current?.dismiss()
71
+ }
72
+ }, [visible])
73
+
74
+ // A sheet unmounted while presented would otherwise stay on screen, blocking everything under it.
75
+ useLayoutEffect(() => {
76
+ const sheet = sheetRef.current
77
+
78
+ return () => {
79
+ if (!presented.current) {
80
+ return
81
+ }
82
+
83
+ presented.current = false
84
+ programmaticDismiss.current = true
85
+ void sheet?.dismiss()
86
+ }
87
+ }, [])
88
+
89
+ const handleDidDismiss = useCallback(() => {
90
+ presented.current = false
91
+
92
+ if (programmaticDismiss.current) {
93
+ programmaticDismiss.current = false
94
+ return
95
+ }
96
+
97
+ onDismiss()
98
+ }, [onDismiss])
99
+
100
+ const handleDetentChange = useCallback(
101
+ (event: DetentChangeEvent) => onDetentChange?.(event.nativeEvent.index),
102
+ [onDetentChange]
103
+ )
104
+
105
+ return (
106
+ <View collapsable={false} pointerEvents="box-none" style={styles.host}>
107
+ <TrueSheet
108
+ backgroundColor={theme.color(bg)}
109
+ cornerRadius={CORNER_RADIUS}
110
+ detents={detents}
111
+ dimmed={dimmed}
112
+ dismissible={dismissible}
113
+ draggable={draggable}
114
+ footer={footer}
115
+ grabber={grabber}
116
+ header={sheetHeader.header}
117
+ headerStyle={sheetHeader.headerStyle}
118
+ onDetentChange={handleDetentChange}
119
+ onDidDismiss={handleDidDismiss}
120
+ onDidPresent={onPresent}
121
+ ref={sheetRef}
122
+ scrollable={scrollable}
123
+ scrollableOptions={sheetHeader.scrollableOptions}
124
+ testID={testID}
125
+ >
126
+ <SheetScrollContext.Provider value={sheetHeader.scrollContext}>
127
+ {sheetHeader.staticLargeTitle}
128
+ {content}
129
+ </SheetScrollContext.Provider>
130
+ </TrueSheet>
131
+ </View>
132
+ )
133
+ }
134
+
135
+ /**
136
+ * A native sheet with an optional native header. `visible` presents and dismisses it; a swipe or a tap outside calls
137
+ * `onDismiss`, which should set `visible` back. `<Sheet.Header>` (a direct child) gives it a bar with a title and
138
+ * items; `<Sheet.ScrollView>` is the body of a `scrollable` sheet.
139
+ */
140
+ export const Sheet: SheetComponent = Object.assign(SheetRoot, {Header: SheetHeader, ScrollView: SheetScrollView})
@@ -1,10 +1,100 @@
1
- import type {ReactNode} from 'react'
1
+ import type {ResponsiveViewSx} from '@hzblj/kara-sx'
2
+ import type {ColorToken} from '@hzblj/kara-theme'
3
+ import type {ReactElement, ReactNode, Ref} from 'react'
4
+ import type {NativeScrollEvent, NativeSyntheticEvent, ScrollViewProps} from 'react-native'
5
+ import type {NavigationBarItem, NavigationBarSearch} from '../../molecules/navigation-bar'
2
6
 
7
+ /** `auto` fits the content; a number is a fraction of the screen's height. */
8
+ export type SheetDetent = 'auto' | number
9
+
10
+ /**
11
+ * `center` is iOS's own, `leading` Material's. `large` puts the title above the content in a large face, and once
12
+ * that scrolls under the bar the bar's own title fades in — centred on iOS, leading on Android.
13
+ */
14
+ export type SheetTitleStyle = 'center' | 'large' | 'leading'
15
+
16
+ /**
17
+ * The sheet's header: a `NavigationBar` with the title and items, and anything else that should stay put above the
18
+ * content (`children` — a segmented control, a search hint). On iOS 26 a scrollable sheet's content scrolls under it
19
+ * with the system's scroll edge effect; elsewhere it sits above the content.
20
+ */
21
+ export type SheetHeaderProps = {
22
+ /** Pinned under the bar. */
23
+ children?: ReactNode
24
+ /** Light items and title, for a sheet with a dark or image background. */
25
+ inverted?: boolean
26
+ /** In screen order, from the leading edge. */
27
+ leadingItems?: NavigationBarItem[]
28
+ /** A search field in place of the title (iOS). */
29
+ search?: NavigationBarSearch
30
+ title?: string
31
+ /** `center` by default. */
32
+ titleStyle?: SheetTitleStyle
33
+ /** In screen order, toward the trailing edge — the close item, by convention. */
34
+ trailingItems?: NavigationBarItem[]
35
+ }
36
+
37
+ export type SheetRef = {
38
+ dismiss: () => Promise<void>
39
+ /** Presents at a detent — the first by default. */
40
+ present: (detentIndex?: number) => Promise<void>
41
+ /** Moves to another detent. */
42
+ resize: (detentIndex: number) => Promise<void>
43
+ }
44
+
45
+ /**
46
+ * A native sheet — `UISheetPresentationController` on iOS, Material's bottom sheet on Android — with an optional
47
+ * native header (`<Sheet.Header>` as a direct child). The React Native tier: it needs no host, and must not sit inside
48
+ * one; native atoms in its content go in an `Island`.
49
+ */
3
50
  export type SheetProps = {
51
+ /** The sheet's colour. `surface.primary` by default. */
52
+ bg?: ColorToken
4
53
  children: ReactNode
5
- fitToContents?: boolean
54
+ /** Where the sheet can rest, smallest first. `['auto']` — fit the content — by default. */
55
+ detents?: SheetDetent[]
56
+ /** Dims what is behind. On by default. */
57
+ dimmed?: boolean
58
+ /** A swipe down or a tap outside closes it. On by default. */
59
+ dismissible?: boolean
60
+ /** It follows the finger between detents. On by default. */
61
+ draggable?: boolean
62
+ /** Pinned to the bottom, above the content and the keyboard. */
63
+ footer?: ReactElement
64
+ /** The drag handle. On by default. */
65
+ grabber?: boolean
66
+ /** The user dismissed it — `visible` going false does not call this. */
6
67
  onDismiss: () => void
68
+ onDetentChange?: (detentIndex: number) => void
69
+ onPresent?: () => void
70
+ ref?: Ref<SheetRef>
71
+ /**
72
+ * The content is a scroll view (`<Sheet.ScrollView>`) the sheet drives: dragging past its top resizes the sheet,
73
+ * and a header scrolls with the iOS 26 edge effect. Off by default — set it with a scrolling body.
74
+ */
75
+ scrollable?: boolean
76
+ testID?: string
7
77
  visible: boolean
8
78
  }
9
79
 
10
- export type SheetComponent = (props: SheetProps) => ReactNode
80
+ export type SheetScrollHandler = (event: NativeSyntheticEvent<NativeScrollEvent>) => void
81
+
82
+ /** What a scroll view inside a sheet needs: the room the header takes, the large title, the scroll listener. */
83
+ export type SheetScrollContextValue = {
84
+ largeTitle: ReactNode
85
+ onScroll?: SheetScrollHandler
86
+ topInset: number
87
+ }
88
+
89
+ export type SheetScrollViewProps = Pick<ScrollViewProps, 'keyboardShouldPersistTaps'> & {
90
+ children: ReactNode
91
+ /** Styles the scrolling content — padding and gap belong here, as with `RnScrollView`. */
92
+ content?: ResponsiveViewSx
93
+ onScroll?: SheetScrollHandler
94
+ testID?: string
95
+ }
96
+
97
+ export type SheetComponent = ((props: SheetProps) => ReactNode) & {
98
+ Header: (props: SheetHeaderProps) => ReactNode
99
+ ScrollView: (props: SheetScrollViewProps) => ReactNode
100
+ }
@@ -0,0 +1,23 @@
1
+ import {Children, isValidElement, type ReactElement, type ReactNode} from 'react'
2
+ import type {SheetHeaderProps} from './sheet.types'
3
+ import {SheetHeader} from './sheet-header'
4
+
5
+ type SheetChildren = {
6
+ content: ReactNode
7
+ header?: SheetHeaderProps
8
+ }
9
+
10
+ const isSheetHeader = (child: ReactNode): child is ReactElement<SheetHeaderProps> =>
11
+ isValidElement(child) && child.type === SheetHeader
12
+
13
+ /** Takes `<Sheet.Header>` out of the children: its props become the native header, the rest is the content. */
14
+ export const splitSheetChildren = (children: ReactNode): SheetChildren => {
15
+ const nodes = Children.toArray(children)
16
+ const header = nodes.find(isSheetHeader)
17
+
18
+ if (!header) {
19
+ return {content: children}
20
+ }
21
+
22
+ return {content: nodes.filter(node => node !== header), header: header.props}
23
+ }
@@ -0,0 +1,135 @@
1
+ import {isScrollEdgeEffectAvailable} from '@hzblj/kara-native'
2
+ import {spacing} from '@hzblj/kara-theme'
3
+ import {useCallback, useMemo, useRef, useState} from 'react'
4
+ import {type LayoutChangeEvent, Platform, type ViewStyle} from 'react-native'
5
+
6
+ import {Box} from '../../atoms/box'
7
+ import {RnText} from '../../atoms/rn-text'
8
+ import {NavigationBar, type NavigationBarTitleAlignment} from '../../molecules/navigation-bar'
9
+ import type {SheetHeaderProps, SheetScrollContextValue, SheetScrollHandler} from './sheet.types'
10
+
11
+ type UseSheetHeaderParams = {
12
+ header?: SheetHeaderProps
13
+ scrollable: boolean
14
+ }
15
+
16
+ /** The bar's own height (`NavigationBar`) plus the room above it, before the header has been measured. */
17
+ const ESTIMATED_HEADER_HEIGHT = (Platform.OS === 'ios' ? 56 : 64) + spacing.s16
18
+
19
+ /** Where a large title's bar title appears once it has scrolled away: the platform's own place for it. */
20
+ const COLLAPSED_TITLE_ALIGNMENT: NavigationBarTitleAlignment = Platform.OS === 'ios' ? 'center' : 'leading'
21
+
22
+ /** Lays the header over the content so the content can scroll under it — the edge effect needs it there. */
23
+ const OVERLAY_HEADER_STYLE: ViewStyle = {left: 0, position: 'absolute', right: 0, top: 0, zIndex: 1}
24
+
25
+ const SOFT_EDGE_EFFECTS = {bottomScrollEdgeEffect: 'soft', topScrollEdgeEffect: 'soft'} as const
26
+
27
+ const SheetLargeTitle = ({onLayout, title}: {onLayout: (event: LayoutChangeEvent) => void; title: string}) => (
28
+ <Box onLayout={onLayout} pb="s8" pt="s8" px="s16">
29
+ <RnText accessibilityRole="header" variant="headingM">
30
+ {title}
31
+ </RnText>
32
+ </Box>
33
+ )
34
+
35
+ /**
36
+ * Turns `<Sheet.Header>`'s props into what the native sheet takes: its header element, how that is laid out, the scroll
37
+ * edge effects, and the context a `<Sheet.ScrollView>` reads. On iOS 26 a scrollable sheet's header is laid over the
38
+ * content (which is then inset by the header's measured height) so the system's soft edge blurs what scrolls under it;
39
+ * everywhere else the header simply sits above the content.
40
+ */
41
+ export const useSheetHeader = ({header: headerProps, scrollable}: UseSheetHeaderParams) => {
42
+ const title = headerProps?.title
43
+ const titleStyle = headerProps?.titleStyle ?? 'center'
44
+ const leadingItems = headerProps?.leadingItems
45
+ const trailingItems = headerProps?.trailingItems
46
+ const inverted = headerProps?.inverted
47
+ const search = headerProps?.search
48
+ const accessory = headerProps?.children
49
+ const hasBar = title !== undefined || !!leadingItems?.length || !!trailingItems?.length || !!search
50
+ const hasHeader = hasBar || !!accessory
51
+ const isOverlay = isScrollEdgeEffectAvailable() && hasHeader && scrollable
52
+ const isLargeTitle = titleStyle === 'large' && !!title
53
+
54
+ const [headerHeight, setHeaderHeight] = useState(hasBar ? ESTIMATED_HEADER_HEIGHT : 0)
55
+ const [largeTitleScrolledAway, setLargeTitleScrolledAway] = useState(false)
56
+ const largeTitleHeight = useRef(0)
57
+
58
+ const handleHeaderLayout = useCallback((event: LayoutChangeEvent) => {
59
+ setHeaderHeight(event.nativeEvent.layout.height)
60
+ }, [])
61
+
62
+ const handleLargeTitleLayout = useCallback((event: LayoutChangeEvent) => {
63
+ largeTitleHeight.current = event.nativeEvent.layout.height
64
+ }, [])
65
+
66
+ const handleScroll = useCallback<SheetScrollHandler>(event => {
67
+ if (largeTitleHeight.current === 0) {
68
+ return
69
+ }
70
+
71
+ setLargeTitleScrolledAway(event.nativeEvent.contentOffset.y >= largeTitleHeight.current)
72
+ }, [])
73
+
74
+ const titleAlignment: NavigationBarTitleAlignment = titleStyle === 'large' ? COLLAPSED_TITLE_ALIGNMENT : titleStyle
75
+ const titleVisible = !isLargeTitle || (scrollable && largeTitleScrolledAway)
76
+
77
+ const headerElement = useMemo(() => {
78
+ if (!hasHeader) {
79
+ return undefined
80
+ }
81
+
82
+ return (
83
+ <Box onLayout={isOverlay ? handleHeaderLayout : undefined} pt="s16">
84
+ {hasBar ? (
85
+ <NavigationBar
86
+ inverted={inverted}
87
+ leadingItems={leadingItems}
88
+ search={search}
89
+ title={title}
90
+ titleAlignment={titleAlignment}
91
+ titleVisible={titleVisible}
92
+ trailingItems={trailingItems}
93
+ />
94
+ ) : null}
95
+ {accessory}
96
+ </Box>
97
+ )
98
+ }, [
99
+ accessory,
100
+ handleHeaderLayout,
101
+ hasBar,
102
+ hasHeader,
103
+ inverted,
104
+ isOverlay,
105
+ leadingItems,
106
+ search,
107
+ title,
108
+ titleAlignment,
109
+ titleVisible,
110
+ trailingItems,
111
+ ])
112
+
113
+ const largeTitle = useMemo(
114
+ () => (title && isLargeTitle ? <SheetLargeTitle onLayout={handleLargeTitleLayout} title={title} /> : null),
115
+ [handleLargeTitleLayout, isLargeTitle, title]
116
+ )
117
+
118
+ const scrollContext = useMemo<SheetScrollContextValue>(
119
+ () => ({
120
+ largeTitle: scrollable ? largeTitle : null,
121
+ onScroll: scrollable && isLargeTitle ? handleScroll : undefined,
122
+ topInset: isOverlay ? headerHeight : 0,
123
+ }),
124
+ [handleScroll, headerHeight, isLargeTitle, isOverlay, largeTitle, scrollable]
125
+ )
126
+
127
+ return {
128
+ header: headerElement,
129
+ headerStyle: isOverlay ? OVERLAY_HEADER_STYLE : undefined,
130
+ scrollableOptions: isOverlay ? SOFT_EDGE_EFFECTS : undefined,
131
+ scrollContext,
132
+ /** Without a scroll view to carry it, a large title simply leads the content. */
133
+ staticLargeTitle: scrollable ? null : largeTitle,
134
+ }
135
+ }
@@ -0,0 +1,47 @@
1
+ import {StyleSheet, type ViewStyle} from 'react-native'
2
+
3
+ /** What lays out a view's children — it goes on the pressable inside, so the content sits where the caller put it. */
4
+ const CONTENT_KEYS = new Set<string>([
5
+ 'alignContent',
6
+ 'alignItems',
7
+ 'columnGap',
8
+ 'flexDirection',
9
+ 'flexWrap',
10
+ 'gap',
11
+ 'justifyContent',
12
+ 'padding',
13
+ 'paddingBottom',
14
+ 'paddingEnd',
15
+ 'paddingHorizontal',
16
+ 'paddingLeft',
17
+ 'paddingRight',
18
+ 'paddingStart',
19
+ 'paddingTop',
20
+ 'paddingVertical',
21
+ 'rowGap',
22
+ ])
23
+
24
+ const FILL: ViewStyle = {flexGrow: 1}
25
+
26
+ /**
27
+ * Splits a resolved style for a pressable with an Android ripple. The ripple is drawn as the pressable's foreground,
28
+ * which its own `overflow: hidden` does not clip — so it would spill past rounded corners. A parent clips its
29
+ * children's foregrounds, though: the outer view takes the shape, background, border and outer layout and clips; the
30
+ * pressable inside takes the padding and the content layout and fills it, so the ripple covers the whole card and
31
+ * stops at its corners.
32
+ */
33
+ export const splitForRipple = (style: ViewStyle | undefined): {inner: ViewStyle[]; outer: ViewStyle} => {
34
+ const flat = StyleSheet.flatten(style) ?? {}
35
+ const outer: Record<string, unknown> = {overflow: 'hidden'}
36
+ const inner: Record<string, unknown> = {}
37
+
38
+ for (const [key, value] of Object.entries(flat)) {
39
+ if (CONTENT_KEYS.has(key)) {
40
+ inner[key] = value
41
+ } else if (key !== 'overflow') {
42
+ outer[key] = value
43
+ }
44
+ }
45
+
46
+ return {inner: [FILL, inner as ViewStyle], outer: outer as ViewStyle}
47
+ }
@@ -0,0 +1,24 @@
1
+ import {clickable, type ModifierConfig, testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
2
+ import {type ResponsiveSx, useSxModifiers} from '@hzblj/kara-sx'
3
+ import {useMemo} from 'react'
4
+
5
+ /**
6
+ * A Compose stack's modifiers in the order a pressable one needs: its frame, shape and background, then the press,
7
+ * then its padding. Compose clips a `clickable` to what comes after it, so with the padding first the ripple would stop
8
+ * at the padding and a row pressed by its edge would light up only in the middle.
9
+ */
10
+ export const useStackModifiers = (sx: ResponsiveSx, onPress?: () => void, testID?: string): ModifierConfig[] => {
11
+ const {p, pb, pl, pr, pt, px, py, ...frame} = sx
12
+ const frameModifiers = useSxModifiers(frame)
13
+ const paddingModifiers = useSxModifiers({p, pb, pl, pr, pt, px, py})
14
+
15
+ return useMemo(
16
+ () => [
17
+ ...frameModifiers,
18
+ ...(testID ? [testIdModifier(testID)] : []),
19
+ ...(onPress ? [clickable(onPress)] : []),
20
+ ...paddingModifiers,
21
+ ],
22
+ [frameModifiers, onPress, paddingModifiers, testID]
23
+ )
24
+ }
@@ -1,26 +0,0 @@
1
- "use strict";
2
-
3
- import { ModalBottomSheet } from '@expo/ui/jetpack-compose';
4
- import { useAppTheme } from '@hzblj/kara-theme';
5
- import { useHostGuard } from "../../host-context.js";
6
- import { jsx as _jsx } from "react/jsx-runtime";
7
- export const Sheet = ({
8
- children,
9
- fitToContents = true,
10
- onDismiss,
11
- visible
12
- }) => {
13
- useHostGuard('Sheet');
14
- const theme = useAppTheme();
15
- if (!visible) {
16
- return null;
17
- }
18
- return /*#__PURE__*/_jsx(ModalBottomSheet, {
19
- containerColor: theme.color('surface.primary'),
20
- contentColor: theme.color('content.primary'),
21
- onDismissRequest: onDismiss,
22
- skipPartiallyExpanded: fitToContents,
23
- children: children
24
- });
25
- };
26
- //# sourceMappingURL=sheet.android.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["ModalBottomSheet","useAppTheme","useHostGuard","jsx","_jsx","Sheet","children","fitToContents","onDismiss","visible","theme","containerColor","color","contentColor","onDismissRequest","skipPartiallyExpanded"],"sourceRoot":"../../../../src","sources":["organisms/sheet/sheet.android.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,QAAO,0BAA0B;AACzD,SAAQC,WAAW,QAAO,mBAAmB;AAE7C,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG/C,OAAO,MAAMC,KAAqB,GAAGA,CAAC;EAACC,QAAQ;EAAEC,aAAa,GAAG,IAAI;EAAEC,SAAS;EAAEC;AAAmB,CAAC,KAAK;EACzGP,YAAY,CAAC,OAAO,CAAC;EACrB,MAAMQ,KAAK,GAAGT,WAAW,CAAC,CAAC;EAE3B,IAAI,CAACQ,OAAO,EAAE;IACZ,OAAO,IAAI;EACb;EAEA,oBACEL,IAAA,CAACJ,gBAAgB;IACfW,cAAc,EAAED,KAAK,CAACE,KAAK,CAAC,iBAAiB,CAAE;IAC/CC,YAAY,EAAEH,KAAK,CAACE,KAAK,CAAC,iBAAiB,CAAE;IAC7CE,gBAAgB,EAAEN,SAAU;IAC5BO,qBAAqB,EAAER,aAAc;IAAAD,QAAA,EAEpCA;EAAQ,CACO,CAAC;AAEvB,CAAC","ignoreList":[]}
@@ -1,25 +0,0 @@
1
- "use strict";
2
-
3
- import { BottomSheet } from '@expo/ui/swift-ui';
4
- import { useHostGuard } from "../../host-context.js";
5
- import { jsx as _jsx } from "react/jsx-runtime";
6
- export const Sheet = ({
7
- children,
8
- fitToContents = true,
9
- onDismiss,
10
- visible
11
- }) => {
12
- useHostGuard('Sheet');
13
- return /*#__PURE__*/_jsx(BottomSheet, {
14
- fitToContents: fitToContents,
15
- isPresented: visible,
16
- onDismiss: onDismiss,
17
- onIsPresentedChange: next => {
18
- if (!next) {
19
- onDismiss();
20
- }
21
- },
22
- children: children
23
- });
24
- };
25
- //# sourceMappingURL=sheet.ios.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["BottomSheet","useHostGuard","jsx","_jsx","Sheet","children","fitToContents","onDismiss","visible","isPresented","onIsPresentedChange","next"],"sourceRoot":"../../../../src","sources":["organisms/sheet/sheet.ios.tsx"],"mappings":";;AAAA,SAAQA,WAAW,QAAO,mBAAmB;AAE7C,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG/C,OAAO,MAAMC,KAAqB,GAAGA,CAAC;EAACC,QAAQ;EAAEC,aAAa,GAAG,IAAI;EAAEC,SAAS;EAAEC;AAAmB,CAAC,KAAK;EACzGP,YAAY,CAAC,OAAO,CAAC;EAErB,oBACEE,IAAA,CAACH,WAAW;IACVM,aAAa,EAAEA,aAAc;IAC7BG,WAAW,EAAED,OAAQ;IACrBD,SAAS,EAAEA,SAAU;IACrBG,mBAAmB,EAAEC,IAAI,IAAI;MAC3B,IAAI,CAACA,IAAI,EAAE;QACTJ,SAAS,CAAC,CAAC;MACb;IACF,CAAE;IAAAF,QAAA,EAEDA;EAAQ,CACE,CAAC;AAElB,CAAC","ignoreList":[]}
@@ -1,2 +0,0 @@
1
- import type { SheetComponent } from './sheet.types';
2
- export declare const Sheet: SheetComponent;
@@ -1,25 +0,0 @@
1
- import {ModalBottomSheet} from '@expo/ui/jetpack-compose'
2
- import {useAppTheme} from '@hzblj/kara-theme'
3
-
4
- import {useHostGuard} from '../../host-context'
5
- import type {SheetComponent, SheetProps} from './sheet.types'
6
-
7
- export const Sheet: SheetComponent = ({children, fitToContents = true, onDismiss, visible}: SheetProps) => {
8
- useHostGuard('Sheet')
9
- const theme = useAppTheme()
10
-
11
- if (!visible) {
12
- return null
13
- }
14
-
15
- return (
16
- <ModalBottomSheet
17
- containerColor={theme.color('surface.primary')}
18
- contentColor={theme.color('content.primary')}
19
- onDismissRequest={onDismiss}
20
- skipPartiallyExpanded={fitToContents}
21
- >
22
- {children}
23
- </ModalBottomSheet>
24
- )
25
- }
@@ -1,23 +0,0 @@
1
- import {BottomSheet} from '@expo/ui/swift-ui'
2
-
3
- import {useHostGuard} from '../../host-context'
4
- import type {SheetComponent, SheetProps} from './sheet.types'
5
-
6
- export const Sheet: SheetComponent = ({children, fitToContents = true, onDismiss, visible}: SheetProps) => {
7
- useHostGuard('Sheet')
8
-
9
- return (
10
- <BottomSheet
11
- fitToContents={fitToContents}
12
- isPresented={visible}
13
- onDismiss={onDismiss}
14
- onIsPresentedChange={next => {
15
- if (!next) {
16
- onDismiss()
17
- }
18
- }}
19
- >
20
- {children}
21
- </BottomSheet>
22
- )
23
- }