@hzblj/kara-components 0.2.1 → 0.3.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 (44) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -3
  3. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +65 -35
  4. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  5. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +48 -13
  6. package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
  7. package/lib/module/organisms/sheet/index.js +2 -1
  8. package/lib/module/organisms/sheet/index.js.map +1 -1
  9. package/lib/module/organisms/sheet/sheet-header.js +7 -0
  10. package/lib/module/organisms/sheet/sheet-header.js.map +1 -0
  11. package/lib/module/organisms/sheet/sheet-scroll.js +62 -0
  12. package/lib/module/organisms/sheet/sheet-scroll.js.map +1 -0
  13. package/lib/module/organisms/sheet/sheet.js +136 -0
  14. package/lib/module/organisms/sheet/sheet.js.map +1 -0
  15. package/lib/module/organisms/sheet/split-sheet-children.js +21 -0
  16. package/lib/module/organisms/sheet/split-sheet-children.js.map +1 -0
  17. package/lib/module/organisms/sheet/use-sheet-header.js +118 -0
  18. package/lib/module/organisms/sheet/use-sheet-header.js.map +1 -0
  19. package/lib/typescript/molecules/navigation-bar/navigation-bar.types.d.ts +56 -17
  20. package/lib/typescript/organisms/sheet/index.d.ts +1 -0
  21. package/lib/typescript/organisms/sheet/sheet-header.d.ts +3 -0
  22. package/lib/typescript/organisms/sheet/sheet-scroll.d.ts +9 -0
  23. package/lib/typescript/organisms/sheet/sheet.d.ts +7 -1
  24. package/lib/typescript/organisms/sheet/sheet.types.d.ts +86 -3
  25. package/lib/typescript/organisms/sheet/split-sheet-children.d.ts +9 -0
  26. package/lib/typescript/organisms/sheet/use-sheet-header.d.ts +24 -0
  27. package/package.json +6 -4
  28. package/src/molecules/navigation-bar/navigation-bar.android.tsx +50 -23
  29. package/src/molecules/navigation-bar/navigation-bar.ios.tsx +54 -15
  30. package/src/molecules/navigation-bar/navigation-bar.types.ts +56 -17
  31. package/src/organisms/sheet/index.ts +1 -0
  32. package/src/organisms/sheet/sheet-header.tsx +6 -0
  33. package/src/organisms/sheet/sheet-scroll.tsx +69 -0
  34. package/src/organisms/sheet/sheet.tsx +140 -0
  35. package/src/organisms/sheet/sheet.types.ts +93 -3
  36. package/src/organisms/sheet/split-sheet-children.ts +23 -0
  37. package/src/organisms/sheet/use-sheet-header.tsx +135 -0
  38. package/lib/module/organisms/sheet/sheet.android.js +0 -26
  39. package/lib/module/organisms/sheet/sheet.android.js.map +0 -1
  40. package/lib/module/organisms/sheet/sheet.ios.js +0 -25
  41. package/lib/module/organisms/sheet/sheet.ios.js.map +0 -1
  42. package/lib/typescript/organisms/sheet/sheet.ios.d.ts +0 -2
  43. package/src/organisms/sheet/sheet.android.tsx +0 -25
  44. package/src/organisms/sheet/sheet.ios.tsx +0 -23
@@ -0,0 +1 @@
1
+ {"version":3,"names":["isScrollEdgeEffectAvailable","spacing","useCallback","useMemo","useRef","useState","Platform","Box","RnText","NavigationBar","jsx","_jsx","jsxs","_jsxs","ESTIMATED_HEADER_HEIGHT","OS","s16","COLLAPSED_TITLE_ALIGNMENT","OVERLAY_HEADER_STYLE","left","position","right","top","zIndex","SOFT_EDGE_EFFECTS","bottomScrollEdgeEffect","topScrollEdgeEffect","SheetLargeTitle","onLayout","title","pb","pt","px","children","accessibilityRole","variant","useSheetHeader","header","headerProps","scrollable","titleStyle","leadingItems","trailingItems","inverted","search","accessory","hasBar","undefined","length","hasHeader","isOverlay","isLargeTitle","headerHeight","setHeaderHeight","largeTitleScrolledAway","setLargeTitleScrolledAway","largeTitleHeight","handleHeaderLayout","event","nativeEvent","layout","height","handleLargeTitleLayout","current","handleScroll","contentOffset","y","titleAlignment","titleVisible","headerElement","largeTitle","scrollContext","onScroll","topInset","headerStyle","scrollableOptions","staticLargeTitle"],"sourceRoot":"../../../../src","sources":["organisms/sheet/use-sheet-header.tsx"],"mappings":"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","ignoreList":[]}
@@ -1,24 +1,63 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode, Ref } from 'react';
2
+ import type { IconName } from '../../atoms/icon';
3
+ type NavigationBarItemBase = {
4
+ onPress: () => void;
5
+ /**
6
+ * iOS 26: neighbouring items on one side share a glass capsule; this gives the item its own. A close next to
7
+ * actions usually wants it. Material has no grouped capsules, so Android ignores it.
8
+ */
9
+ separated?: boolean;
10
+ /** iOS: the item's accessibility identifier; Android: its test tag. */
11
+ testID?: string;
12
+ };
13
+ /**
14
+ * One item of the bar. `back` and `close` are the platform's own — a chevron and the system close item on iOS, the
15
+ * Material arrow and cross on Android — with `Back` / `Close` as their labels unless given others. `icon` is one of the
16
+ * app's icons and names itself for VoiceOver and TalkBack.
17
+ */
18
+ export type NavigationBarItem = (NavigationBarItemBase & {
19
+ label?: string;
20
+ type: 'back' | 'close';
21
+ }) | (NavigationBarItemBase & {
22
+ icon: IconName;
23
+ label: string;
24
+ type: 'icon';
25
+ });
26
+ export type NavigationBarSearchRef = {
27
+ /** Empties the field and ends editing. */
28
+ clear: () => void;
29
+ };
30
+ /** A search field in place of the title — iOS only for now; Android keeps the title. */
31
+ export type NavigationBarSearch = {
32
+ onChangeText: (text: string) => void;
33
+ onFocusChange?: (focused: boolean) => void;
34
+ placeholder: string;
35
+ ref?: Ref<NavigationBarSearchRef>;
36
+ testID?: string;
37
+ };
38
+ export type NavigationBarTitleAlignment = 'center' | 'leading';
2
39
  /**
3
- * A title with back and close items, for a sheet that is not a navigation screen. On iOS a standalone
4
- * `UINavigationBar`, so the items press and render exactly like a native-stack header's — on iOS 26 they share the
5
- * bar's glass instead of swelling as standalone glass buttons do. On Android a Material row of icon buttons. The React
6
- * Native tier: it needs no host.
40
+ * A title with items on either side, for a header that is not a navigation screen's — `Sheet.Header` is built on it.
41
+ * On iOS a standalone `UINavigationBar`, so the items press and render exactly like a native-stack header's: on iOS 26
42
+ * neighbours share a glass capsule instead of swelling as standalone glass buttons do. On Android Material's top app
43
+ * bar. Where an item goes is the caller's choice — the bar places nothing by itself, so a close can sit on either side.
44
+ * The React Native tier: it needs no host.
7
45
  */
8
46
  export type NavigationBarProps = {
9
- /** The back item's accessibility label. `Back` by default. */
10
- backLabel?: string;
11
- /** iOS: the back item's accessibility identifier. */
12
- backTestID?: string;
13
- /** The close item's accessibility label. `Close` by default. */
14
- closeLabel?: string;
15
- /** iOS: the close item's accessibility identifier. */
16
- closeTestID?: string;
17
- /** Shows the back item. */
18
- onBack?: () => void;
19
- /** Shows the close item. */
20
- onClose?: () => void;
47
+ /** Light items and title, for a bar laid over an image or a dark hero. */
48
+ inverted?: boolean;
49
+ /** In screen order, from the leading edge. */
50
+ leadingItems?: NavigationBarItem[];
51
+ /** Replaces the title with a search field (iOS). */
52
+ search?: NavigationBarSearch;
21
53
  testID?: string;
22
54
  title?: string;
55
+ /** `center` by default — iOS's own; `leading` is Material's top app bar. */
56
+ titleAlignment?: NavigationBarTitleAlignment;
57
+ /** Fades the title out and in without moving the items — for a large title that scrolls under the bar. */
58
+ titleVisible?: boolean;
59
+ /** In screen order, toward the trailing edge. */
60
+ trailingItems?: NavigationBarItem[];
23
61
  };
24
62
  export type NavigationBarComponent = (props: NavigationBarProps) => ReactNode;
63
+ export {};
@@ -1,2 +1,3 @@
1
1
  export * from './sheet';
2
2
  export * from './sheet.types';
3
+ export { useSheetScroll } from './sheet-scroll';
@@ -0,0 +1,3 @@
1
+ import type { SheetHeaderProps } from './sheet.types';
2
+ /** A slot, never rendered: `Sheet` reads its props and builds the header into the native sheet's own header area. */
3
+ export declare const SheetHeader: (_props: SheetHeaderProps) => never;
@@ -0,0 +1,9 @@
1
+ import type { SheetScrollContextValue, SheetScrollViewProps } from './sheet.types';
2
+ export declare const SheetScrollContext: import("react").Context<SheetScrollContextValue | null>;
3
+ /**
4
+ * For a scroll view of your own inside a sheet (a `RnFlatList`, say): pad its content by `topInset`, render
5
+ * `largeTitle` first, and forward `onScroll` — which is what `<Sheet.ScrollView>` does.
6
+ */
7
+ export declare const useSheetScroll: () => SheetScrollContextValue;
8
+ /** The sheet's scrolling body: starts below the header on iOS 26, carries a large title, and reports its scroll. */
9
+ export declare const SheetScrollView: ({ children, content, keyboardShouldPersistTaps, onScroll, testID, }: SheetScrollViewProps) => import("react").JSX.Element;
@@ -1 +1,7 @@
1
- export * from './sheet.ios';
1
+ import type { SheetComponent } from './sheet.types';
2
+ /**
3
+ * A native sheet with an optional native header. `visible` presents and dismisses it; a swipe or a tap outside calls
4
+ * `onDismiss`, which should set `visible` back. `<Sheet.Header>` (a direct child) gives it a bar with a title and
5
+ * items; `<Sheet.ScrollView>` is the body of a `scrollable` sheet.
6
+ */
7
+ export declare const Sheet: SheetComponent;
@@ -1,8 +1,91 @@
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';
6
+ /** `auto` fits the content; a number is a fraction of the screen's height. */
7
+ export type SheetDetent = 'auto' | number;
8
+ /**
9
+ * `center` is iOS's own, `leading` Material's. `large` puts the title above the content in a large face, and once
10
+ * that scrolls under the bar the bar's own title fades in — centred on iOS, leading on Android.
11
+ */
12
+ export type SheetTitleStyle = 'center' | 'large' | 'leading';
13
+ /**
14
+ * The sheet's header: a `NavigationBar` with the title and items, and anything else that should stay put above the
15
+ * content (`children` — a segmented control, a search hint). On iOS 26 a scrollable sheet's content scrolls under it
16
+ * with the system's scroll edge effect; elsewhere it sits above the content.
17
+ */
18
+ export type SheetHeaderProps = {
19
+ /** Pinned under the bar. */
20
+ children?: ReactNode;
21
+ /** Light items and title, for a sheet with a dark or image background. */
22
+ inverted?: boolean;
23
+ /** In screen order, from the leading edge. */
24
+ leadingItems?: NavigationBarItem[];
25
+ /** A search field in place of the title (iOS). */
26
+ search?: NavigationBarSearch;
27
+ title?: string;
28
+ /** `center` by default. */
29
+ titleStyle?: SheetTitleStyle;
30
+ /** In screen order, toward the trailing edge — the close item, by convention. */
31
+ trailingItems?: NavigationBarItem[];
32
+ };
33
+ export type SheetRef = {
34
+ dismiss: () => Promise<void>;
35
+ /** Presents at a detent — the first by default. */
36
+ present: (detentIndex?: number) => Promise<void>;
37
+ /** Moves to another detent. */
38
+ resize: (detentIndex: number) => Promise<void>;
39
+ };
40
+ /**
41
+ * A native sheet — `UISheetPresentationController` on iOS, Material's bottom sheet on Android — with an optional
42
+ * native header (`<Sheet.Header>` as a direct child). The React Native tier: it needs no host, and must not sit inside
43
+ * one; native atoms in its content go in an `Island`.
44
+ */
2
45
  export type SheetProps = {
46
+ /** The sheet's colour. `surface.primary` by default. */
47
+ bg?: ColorToken;
3
48
  children: ReactNode;
4
- fitToContents?: boolean;
49
+ /** Where the sheet can rest, smallest first. `['auto']` — fit the content — by default. */
50
+ detents?: SheetDetent[];
51
+ /** Dims what is behind. On by default. */
52
+ dimmed?: boolean;
53
+ /** A swipe down or a tap outside closes it. On by default. */
54
+ dismissible?: boolean;
55
+ /** It follows the finger between detents. On by default. */
56
+ draggable?: boolean;
57
+ /** Pinned to the bottom, above the content and the keyboard. */
58
+ footer?: ReactElement;
59
+ /** The drag handle. On by default. */
60
+ grabber?: boolean;
61
+ /** The user dismissed it — `visible` going false does not call this. */
5
62
  onDismiss: () => void;
63
+ onDetentChange?: (detentIndex: number) => void;
64
+ onPresent?: () => void;
65
+ ref?: Ref<SheetRef>;
66
+ /**
67
+ * The content is a scroll view (`<Sheet.ScrollView>`) the sheet drives: dragging past its top resizes the sheet,
68
+ * and a header scrolls with the iOS 26 edge effect. Off by default — set it with a scrolling body.
69
+ */
70
+ scrollable?: boolean;
71
+ testID?: string;
6
72
  visible: boolean;
7
73
  };
8
- export type SheetComponent = (props: SheetProps) => ReactNode;
74
+ export type SheetScrollHandler = (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
75
+ /** What a scroll view inside a sheet needs: the room the header takes, the large title, the scroll listener. */
76
+ export type SheetScrollContextValue = {
77
+ largeTitle: ReactNode;
78
+ onScroll?: SheetScrollHandler;
79
+ topInset: number;
80
+ };
81
+ export type SheetScrollViewProps = Pick<ScrollViewProps, 'keyboardShouldPersistTaps'> & {
82
+ children: ReactNode;
83
+ /** Styles the scrolling content — padding and gap belong here, as with `RnScrollView`. */
84
+ content?: ResponsiveViewSx;
85
+ onScroll?: SheetScrollHandler;
86
+ testID?: string;
87
+ };
88
+ export type SheetComponent = ((props: SheetProps) => ReactNode) & {
89
+ Header: (props: SheetHeaderProps) => ReactNode;
90
+ ScrollView: (props: SheetScrollViewProps) => ReactNode;
91
+ };
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { SheetHeaderProps } from './sheet.types';
3
+ type SheetChildren = {
4
+ content: ReactNode;
5
+ header?: SheetHeaderProps;
6
+ };
7
+ /** Takes `<Sheet.Header>` out of the children: its props become the native header, the rest is the content. */
8
+ export declare const splitSheetChildren: (children: ReactNode) => SheetChildren;
9
+ export {};
@@ -0,0 +1,24 @@
1
+ import { type ViewStyle } from 'react-native';
2
+ import type { SheetHeaderProps, SheetScrollContextValue } from './sheet.types';
3
+ type UseSheetHeaderParams = {
4
+ header?: SheetHeaderProps;
5
+ scrollable: boolean;
6
+ };
7
+ /**
8
+ * Turns `<Sheet.Header>`'s props into what the native sheet takes: its header element, how that is laid out, the scroll
9
+ * edge effects, and the context a `<Sheet.ScrollView>` reads. On iOS 26 a scrollable sheet's header is laid over the
10
+ * content (which is then inset by the header's measured height) so the system's soft edge blurs what scrolls under it;
11
+ * everywhere else the header simply sits above the content.
12
+ */
13
+ export declare const useSheetHeader: ({ header: headerProps, scrollable }: UseSheetHeaderParams) => {
14
+ header: import("react").JSX.Element | undefined;
15
+ headerStyle: ViewStyle | undefined;
16
+ scrollableOptions: {
17
+ readonly bottomScrollEdgeEffect: 'soft';
18
+ readonly topScrollEdgeEffect: 'soft';
19
+ } | undefined;
20
+ scrollContext: SheetScrollContextValue;
21
+ /** Without a scroll view to carry it, a large title simply leads the content. */
22
+ staticLargeTitle: import("react").JSX.Element | null;
23
+ };
24
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
5
5
  "author": "Jan Blazej",
6
6
  "license": "MIT",
@@ -59,13 +59,14 @@
59
59
  ]
60
60
  },
61
61
  "dependencies": {
62
- "@hzblj/kara-native": "^0.2.1",
63
- "@hzblj/kara-sx": "^0.2.1",
64
- "@hzblj/kara-theme": "^0.2.1",
62
+ "@hzblj/kara-native": "^0.3.0",
63
+ "@hzblj/kara-sx": "^0.3.0",
64
+ "@hzblj/kara-theme": "^0.3.0",
65
65
  "sf-symbols-typescript": "^2.2.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@expo/ui": "~57.0.0",
69
+ "@lodev09/react-native-true-sheet": ">=3.11.0",
69
70
  "@shopify/flash-list": ">=2.0.0",
70
71
  "expo": "~57.0.0",
71
72
  "expo-haptics": "*",
@@ -76,6 +77,7 @@
76
77
  },
77
78
  "devDependencies": {
78
79
  "@expo/ui": "~57.0.18",
80
+ "@lodev09/react-native-true-sheet": "3.11.18",
79
81
  "@shopify/flash-list": "^2.3.2",
80
82
  "@storybook/react-native": "^10.6.0",
81
83
  "@types/react": "~19.2.18",
@@ -1,28 +1,55 @@
1
- import {HStack} from '../../atoms/h-stack'
1
+ import {iconResourceName} from '@hzblj/kara-native'
2
+ import {TopAppBar, type TopAppBarItem} from '@hzblj/kara-native/jetpack-compose'
3
+ import {useAppTheme} from '@hzblj/kara-theme'
4
+ import {useMemo} from 'react'
5
+ import {StyleSheet} from 'react-native'
2
6
  import {Island} from '../../atoms/island'
3
- import {Spacer} from '../../atoms/spacer'
4
- import {Text} from '../../atoms/text'
5
- import {GlyphButton} from '../../glyph'
6
- import type {NavigationBarComponent, NavigationBarProps} from './navigation-bar.types'
7
+ import type {NavigationBarComponent, NavigationBarItem, NavigationBarProps} from './navigation-bar.types'
7
8
 
8
- /**
9
- * Material's top bar shape — navigation icon, start-aligned title, action at the end — built from the atoms in its own
10
- * island. Compose's icon buttons take no accessibility identifier, so `backTestID`/`closeTestID` are iOS-only.
11
- */
9
+ /** Material's small top app bar. */
10
+ const BAR_HEIGHT = 64
11
+
12
+ const styles = StyleSheet.create({
13
+ bar: {height: BAR_HEIGHT, width: '100%'},
14
+ })
15
+
16
+ const DEFAULT_LABELS = {back: 'Back', close: 'Close'} as const
17
+
18
+ const toNativeItem = ({onPress, testID, ...item}: NavigationBarItem): TopAppBarItem =>
19
+ item.type === 'icon'
20
+ ? {iconName: iconResourceName(item.icon), label: item.label, onPress, testID, type: 'icon'}
21
+ : {label: item.label ?? DEFAULT_LABELS[item.type], onPress, testID, type: item.type}
22
+
23
+ /** Material's top app bar in its own host. `search` is iOS-only for now, and `separated` has no Material counterpart. */
12
24
  export const NavigationBar: NavigationBarComponent = ({
13
- backLabel = 'Back',
14
- closeLabel = 'Close',
15
- onBack,
16
- onClose,
25
+ inverted = false,
26
+ leadingItems,
17
27
  testID,
18
28
  title,
19
- }: NavigationBarProps) => (
20
- <Island testID={testID}>
21
- <HStack align="center" px="s4" spacing="s4" wide>
22
- {onBack ? <GlyphButton accessibilityLabel={backLabel} name="back" onPress={onBack} /> : null}
23
- {title ? <Text variant="body">{title}</Text> : null}
24
- <Spacer />
25
- {onClose ? <GlyphButton accessibilityLabel={closeLabel} name="close" onPress={onClose} /> : null}
26
- </HStack>
27
- </Island>
28
- )
29
+ titleAlignment = 'center',
30
+ titleVisible = true,
31
+ trailingItems,
32
+ }: NavigationBarProps) => {
33
+ const theme = useAppTheme()
34
+ const font = theme.typography.headline
35
+ const color = theme.color(inverted ? 'content.on.bg' : 'content.primary')
36
+
37
+ const nativeLeadingItems = useMemo(() => leadingItems?.map(toNativeItem), [leadingItems])
38
+ const nativeTrailingItems = useMemo(() => trailingItems?.map(toNativeItem), [trailingItems])
39
+
40
+ return (
41
+ <Island matchContents={false} style={styles.bar} testID={testID}>
42
+ <TopAppBar
43
+ fontFamily={font.fontFamily}
44
+ fontSize={font.fontSize}
45
+ itemColor={color}
46
+ leadingItems={nativeLeadingItems}
47
+ title={title}
48
+ titleAlignment={titleAlignment}
49
+ titleColor={color}
50
+ titleVisible={titleVisible}
51
+ trailingItems={nativeTrailingItems}
52
+ />
53
+ </Island>
54
+ )
55
+ }
@@ -1,33 +1,72 @@
1
- import {NavigationBar as NativeNavigationBar} from '@hzblj/kara-native/swift-ui'
1
+ import {iconResourceName} from '@hzblj/kara-native'
2
+ import {
3
+ NavigationBar as NativeNavigationBar,
4
+ type NavigationBarItem as NativeNavigationBarItem,
5
+ type NavigationBarRef,
6
+ } from '@hzblj/kara-native/swift-ui'
2
7
  import {useAppTheme} from '@hzblj/kara-theme'
8
+ import {useImperativeHandle, useMemo, useRef} from 'react'
3
9
  import {StyleSheet} from 'react-native'
4
- import type {NavigationBarComponent, NavigationBarProps} from './navigation-bar.types'
10
+ import type {NavigationBarComponent, NavigationBarItem, NavigationBarProps} from './navigation-bar.types'
5
11
 
6
- /** UIKit's compact bar height. The bar centres itself vertically in whatever it is given. */
7
- const BAR_HEIGHT = 44
12
+ /** Fits iOS 26's 44 pt glass items with the bar's own margin; the bar centres itself vertically in it. */
13
+ const BAR_HEIGHT = 56
8
14
 
9
15
  const styles = StyleSheet.create({
10
16
  bar: {height: BAR_HEIGHT, width: '100%'},
11
17
  })
12
18
 
19
+ const DEFAULT_LABELS = {back: 'Back', close: 'Close'} as const
20
+
21
+ const toNativeItem = (item: NavigationBarItem): NativeNavigationBarItem =>
22
+ item.type === 'icon'
23
+ ? {...item, imageName: iconResourceName(item.icon), type: 'icon'}
24
+ : {...item, label: item.label ?? DEFAULT_LABELS[item.type]}
25
+
26
+ const ignoreDetachedView = () => undefined
27
+
13
28
  export const NavigationBar: NavigationBarComponent = ({
14
- backLabel = 'Back',
15
- closeLabel = 'Close',
16
- ...props
29
+ inverted = false,
30
+ leadingItems,
31
+ search,
32
+ testID,
33
+ title,
34
+ titleAlignment = 'center',
35
+ titleVisible = true,
36
+ trailingItems,
17
37
  }: NavigationBarProps) => {
18
38
  const theme = useAppTheme()
19
- const title = theme.typography.body
39
+ const barRef = useRef<NavigationBarRef>(null)
40
+ const font = theme.typography.headline
41
+ const color = theme.color(inverted ? 'content.on.bg' : 'content.primary')
42
+
43
+ useImperativeHandle(
44
+ search?.ref,
45
+ () => ({clear: () => barRef.current?.clearSearchText().catch(ignoreDetachedView)}),
46
+ []
47
+ )
48
+
49
+ const nativeLeadingItems = useMemo(() => leadingItems?.map(toNativeItem), [leadingItems])
50
+ const nativeTrailingItems = useMemo(() => trailingItems?.map(toNativeItem), [trailingItems])
20
51
 
21
52
  return (
22
53
  <NativeNavigationBar
23
- {...props}
24
- backLabel={backLabel}
25
- closeLabel={closeLabel}
26
- fontFamily={title.fontFamily}
27
- fontSize={title.fontSize}
28
- itemColor={theme.color('content.primary')}
54
+ fontFamily={font.fontFamily}
55
+ fontSize={font.fontSize}
56
+ itemColor={color}
57
+ leadingItems={nativeLeadingItems}
58
+ onSearchFocusChange={search?.onFocusChange}
59
+ onSearchTextChange={search?.onChangeText}
60
+ ref={barRef}
61
+ searchPlaceholder={search?.placeholder}
62
+ searchTestID={search?.testID}
29
63
  style={styles.bar}
30
- titleColor={theme.color('content.primary')}
64
+ testID={testID}
65
+ title={title}
66
+ titleAlignment={titleAlignment}
67
+ titleColor={color}
68
+ titleVisible={titleVisible}
69
+ trailingItems={nativeTrailingItems}
31
70
  />
32
71
  )
33
72
  }
@@ -1,26 +1,65 @@
1
- import type {ReactNode} from 'react'
1
+ import type {ReactNode, Ref} from 'react'
2
+
3
+ import type {IconName} from '../../atoms/icon'
4
+
5
+ type NavigationBarItemBase = {
6
+ onPress: () => void
7
+ /**
8
+ * iOS 26: neighbouring items on one side share a glass capsule; this gives the item its own. A close next to
9
+ * actions usually wants it. Material has no grouped capsules, so Android ignores it.
10
+ */
11
+ separated?: boolean
12
+ /** iOS: the item's accessibility identifier; Android: its test tag. */
13
+ testID?: string
14
+ }
15
+
16
+ /**
17
+ * One item of the bar. `back` and `close` are the platform's own — a chevron and the system close item on iOS, the
18
+ * Material arrow and cross on Android — with `Back` / `Close` as their labels unless given others. `icon` is one of the
19
+ * app's icons and names itself for VoiceOver and TalkBack.
20
+ */
21
+ export type NavigationBarItem =
22
+ | (NavigationBarItemBase & {label?: string; type: 'back' | 'close'})
23
+ | (NavigationBarItemBase & {icon: IconName; label: string; type: 'icon'})
24
+
25
+ export type NavigationBarSearchRef = {
26
+ /** Empties the field and ends editing. */
27
+ clear: () => void
28
+ }
29
+
30
+ /** A search field in place of the title — iOS only for now; Android keeps the title. */
31
+ export type NavigationBarSearch = {
32
+ onChangeText: (text: string) => void
33
+ onFocusChange?: (focused: boolean) => void
34
+ placeholder: string
35
+ ref?: Ref<NavigationBarSearchRef>
36
+ testID?: string
37
+ }
38
+
39
+ export type NavigationBarTitleAlignment = 'center' | 'leading'
2
40
 
3
41
  /**
4
- * A title with back and close items, for a sheet that is not a navigation screen. On iOS a standalone
5
- * `UINavigationBar`, so the items press and render exactly like a native-stack header's — on iOS 26 they share the
6
- * bar's glass instead of swelling as standalone glass buttons do. On Android a Material row of icon buttons. The React
7
- * Native tier: it needs no host.
42
+ * A title with items on either side, for a header that is not a navigation screen's — `Sheet.Header` is built on it.
43
+ * On iOS a standalone `UINavigationBar`, so the items press and render exactly like a native-stack header's: on iOS 26
44
+ * neighbours share a glass capsule instead of swelling as standalone glass buttons do. On Android Material's top app
45
+ * bar. Where an item goes is the caller's choice — the bar places nothing by itself, so a close can sit on either side.
46
+ * The React Native tier: it needs no host.
8
47
  */
9
48
  export type NavigationBarProps = {
10
- /** The back item's accessibility label. `Back` by default. */
11
- backLabel?: string
12
- /** iOS: the back item's accessibility identifier. */
13
- backTestID?: string
14
- /** The close item's accessibility label. `Close` by default. */
15
- closeLabel?: string
16
- /** iOS: the close item's accessibility identifier. */
17
- closeTestID?: string
18
- /** Shows the back item. */
19
- onBack?: () => void
20
- /** Shows the close item. */
21
- onClose?: () => void
49
+ /** Light items and title, for a bar laid over an image or a dark hero. */
50
+ inverted?: boolean
51
+ /** In screen order, from the leading edge. */
52
+ leadingItems?: NavigationBarItem[]
53
+ /** Replaces the title with a search field (iOS). */
54
+ search?: NavigationBarSearch
22
55
  testID?: string
23
56
  title?: string
57
+ /** `center` by default — iOS's own; `leading` is Material's top app bar. */
58
+ titleAlignment?: NavigationBarTitleAlignment
59
+ /** Fades the title out and in without moving the items — for a large title that scrolls under the bar. */
60
+ titleVisible?: boolean
61
+ /** In screen order, toward the trailing edge. */
62
+ trailingItems?: NavigationBarItem[]
24
63
  }
25
64
 
26
65
  export type NavigationBarComponent = (props: NavigationBarProps) => ReactNode
@@ -1,2 +1,3 @@
1
1
  export * from './sheet'
2
2
  export * from './sheet.types'
3
+ export {useSheetScroll} from './sheet-scroll'
@@ -0,0 +1,6 @@
1
+ import type {SheetHeaderProps} from './sheet.types'
2
+
3
+ /** A slot, never rendered: `Sheet` reads its props and builds the header into the native sheet's own header area. */
4
+ export const SheetHeader = (_props: SheetHeaderProps): never => {
5
+ throw new Error('[kara-ui] <Sheet.Header> must be a direct child of <Sheet>.')
6
+ }
@@ -0,0 +1,69 @@
1
+ import {useSx} from '@hzblj/kara-sx'
2
+ import {createContext, useCallback, useContext, useMemo} from 'react'
3
+ import {ScrollView} from 'react-native'
4
+
5
+ import type {SheetScrollContextValue, SheetScrollHandler, SheetScrollViewProps} from './sheet.types'
6
+
7
+ export const SheetScrollContext = createContext<SheetScrollContextValue | null>(null)
8
+
9
+ /**
10
+ * For a scroll view of your own inside a sheet (a `RnFlatList`, say): pad its content by `topInset`, render
11
+ * `largeTitle` first, and forward `onScroll` — which is what `<Sheet.ScrollView>` does.
12
+ */
13
+ export const useSheetScroll = (): SheetScrollContextValue => {
14
+ const context = useContext(SheetScrollContext)
15
+
16
+ if (context === null) {
17
+ throw new Error('[kara-ui] useSheetScroll and <Sheet.ScrollView> must be used inside <Sheet>.')
18
+ }
19
+
20
+ return context
21
+ }
22
+
23
+ const NO_SX = {}
24
+
25
+ /** The sheet's scrolling body: starts below the header on iOS 26, carries a large title, and reports its scroll. */
26
+ export const SheetScrollView = ({
27
+ children,
28
+ content,
29
+ keyboardShouldPersistTaps,
30
+ onScroll,
31
+ testID,
32
+ }: SheetScrollViewProps) => {
33
+ const {largeTitle, onScroll: onSheetScroll, topInset} = useSheetScroll()
34
+ const contentStyle = useSx(content ?? NO_SX)
35
+ // The header's room goes on top of the content's own top padding, not in place of it.
36
+ const containerStyle = useMemo(() => {
37
+ if (topInset === 0) {
38
+ return contentStyle
39
+ }
40
+
41
+ const ownTop = typeof contentStyle.paddingTop === 'number' ? contentStyle.paddingTop : 0
42
+
43
+ return [contentStyle, {paddingTop: topInset + ownTop}]
44
+ }, [contentStyle, topInset])
45
+
46
+ const handleScroll = useCallback<SheetScrollHandler>(
47
+ event => {
48
+ onSheetScroll?.(event)
49
+ onScroll?.(event)
50
+ },
51
+ [onScroll, onSheetScroll]
52
+ )
53
+
54
+ return (
55
+ <ScrollView
56
+ contentContainerStyle={containerStyle}
57
+ contentInsetAdjustmentBehavior="never"
58
+ keyboardShouldPersistTaps={keyboardShouldPersistTaps}
59
+ nestedScrollEnabled
60
+ onScroll={onSheetScroll || onScroll ? handleScroll : undefined}
61
+ overScrollMode="always"
62
+ scrollEventThrottle={16}
63
+ testID={testID}
64
+ >
65
+ {largeTitle}
66
+ {children}
67
+ </ScrollView>
68
+ )
69
+ }