@hzblj/kara-components 0.7.0 → 0.8.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 (117) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +5 -5
  3. package/lib/module/atoms/activity-indicator/activity-indicator.android.js +42 -0
  4. package/lib/module/atoms/activity-indicator/activity-indicator.android.js.map +1 -0
  5. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js +19 -0
  6. package/lib/module/atoms/activity-indicator/activity-indicator.ios.js.map +1 -0
  7. package/lib/module/atoms/activity-indicator/activity-indicator.types.js +4 -0
  8. package/lib/module/atoms/activity-indicator/activity-indicator.types.js.map +1 -0
  9. package/lib/module/atoms/activity-indicator/index.js +2 -1
  10. package/lib/module/atoms/activity-indicator/index.js.map +1 -1
  11. package/lib/module/atoms/avatar/avatar.js +2 -4
  12. package/lib/module/atoms/avatar/avatar.js.map +1 -1
  13. package/lib/module/atoms/blur-view/blur-view.android.js +100 -0
  14. package/lib/module/atoms/blur-view/blur-view.android.js.map +1 -0
  15. package/lib/module/atoms/blur-view/blur-view.ios.js +43 -0
  16. package/lib/module/atoms/blur-view/blur-view.ios.js.map +1 -0
  17. package/lib/module/atoms/blur-view/blur-view.types.js +4 -0
  18. package/lib/module/atoms/blur-view/blur-view.types.js.map +1 -0
  19. package/lib/module/atoms/blur-view/index.js +5 -0
  20. package/lib/module/atoms/blur-view/index.js.map +1 -0
  21. package/lib/module/atoms/glass-view/glass-view.android.js +36 -5
  22. package/lib/module/atoms/glass-view/glass-view.android.js.map +1 -1
  23. package/lib/module/atoms/image/image.js +13 -6
  24. package/lib/module/atoms/image/image.js.map +1 -1
  25. package/lib/module/atoms/image/image.types.js +4 -0
  26. package/lib/module/atoms/image/image.types.js.map +1 -0
  27. package/lib/module/atoms/image/index.js +1 -0
  28. package/lib/module/atoms/image/index.js.map +1 -1
  29. package/lib/module/atoms/index.js +1 -0
  30. package/lib/module/atoms/index.js.map +1 -1
  31. package/lib/module/atoms/island/island.android.js +6 -2
  32. package/lib/module/atoms/island/island.android.js.map +1 -1
  33. package/lib/module/atoms/island/island.ios.js +5 -2
  34. package/lib/module/atoms/island/island.ios.js.map +1 -1
  35. package/lib/module/atoms/pressable/pressable.js +38 -6
  36. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  37. package/lib/module/atoms/rn-text/rn-text.js +5 -2
  38. package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
  39. package/lib/module/atoms/skeleton/skeleton.js +5 -5
  40. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  41. package/lib/module/atoms/text/text.android.js +5 -2
  42. package/lib/module/atoms/text/text.android.js.map +1 -1
  43. package/lib/module/atoms/text/text.ios.js +14 -3
  44. package/lib/module/atoms/text/text.ios.js.map +1 -1
  45. package/lib/module/molecules/collapsible/collapsible.android.js +37 -7
  46. package/lib/module/molecules/collapsible/collapsible.android.js.map +1 -1
  47. package/lib/module/molecules/navigation-bar/navigation-bar.android.js +2 -8
  48. package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
  49. package/lib/module/molecules/pager/pager.android.js +5 -4
  50. package/lib/module/molecules/pager/pager.android.js.map +1 -1
  51. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +114 -11
  52. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  53. package/lib/module/molecules/stepper/stepper.ios.js +45 -8
  54. package/lib/module/molecules/stepper/stepper.ios.js.map +1 -1
  55. package/lib/module/ripple-clip.js +9 -1
  56. package/lib/module/ripple-clip.js.map +1 -1
  57. package/lib/typescript/atoms/activity-indicator/activity-indicator.d.ts +1 -3
  58. package/lib/typescript/atoms/activity-indicator/activity-indicator.ios.d.ts +3 -0
  59. package/lib/typescript/atoms/activity-indicator/activity-indicator.types.d.ts +17 -0
  60. package/lib/typescript/atoms/activity-indicator/index.d.ts +1 -0
  61. package/lib/typescript/atoms/blur-view/blur-view.d.ts +1 -0
  62. package/lib/typescript/atoms/blur-view/blur-view.ios.d.ts +3 -0
  63. package/lib/typescript/atoms/blur-view/blur-view.types.d.ts +31 -0
  64. package/lib/typescript/atoms/blur-view/index.d.ts +2 -0
  65. package/lib/typescript/atoms/box/a-box.d.ts +1 -1
  66. package/lib/typescript/atoms/glass-view/glass-view.types.d.ts +12 -5
  67. package/lib/typescript/atoms/image/image.d.ts +12 -4
  68. package/lib/typescript/atoms/image/image.types.d.ts +4 -0
  69. package/lib/typescript/atoms/image/index.d.ts +1 -0
  70. package/lib/typescript/atoms/index.d.ts +1 -0
  71. package/lib/typescript/atoms/island/island.types.d.ts +8 -4
  72. package/lib/typescript/atoms/pressable/pressable.types.d.ts +5 -3
  73. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -2
  74. package/lib/typescript/atoms/skeleton/skeleton.types.d.ts +8 -4
  75. package/lib/typescript/atoms/text/text.types.d.ts +7 -2
  76. package/lib/typescript/molecules/scroll-edge-container/scroll-edge-container.types.d.ts +6 -4
  77. package/lib/typescript/molecules/stepper/stepper.ios.d.ts +5 -0
  78. package/lib/typescript/molecules/stepper/stepper.types.d.ts +1 -4
  79. package/package.json +5 -5
  80. package/src/atoms/activity-indicator/activity-indicator.android.tsx +32 -0
  81. package/src/atoms/activity-indicator/activity-indicator.ios.tsx +15 -0
  82. package/src/atoms/activity-indicator/activity-indicator.types.ts +20 -0
  83. package/src/atoms/activity-indicator/index.ts +1 -0
  84. package/src/atoms/avatar/avatar.tsx +1 -1
  85. package/src/atoms/blur-view/blur-view.android.tsx +68 -0
  86. package/src/atoms/blur-view/blur-view.ios.tsx +37 -0
  87. package/src/atoms/blur-view/blur-view.types.ts +34 -0
  88. package/src/atoms/blur-view/index.ts +2 -0
  89. package/src/atoms/glass-view/glass-view.android.tsx +41 -3
  90. package/src/atoms/glass-view/glass-view.types.ts +12 -5
  91. package/src/atoms/image/image.tsx +11 -12
  92. package/src/atoms/image/image.types.ts +6 -0
  93. package/src/atoms/image/index.ts +1 -0
  94. package/src/atoms/index.ts +1 -0
  95. package/src/atoms/island/island.android.tsx +10 -2
  96. package/src/atoms/island/island.ios.tsx +4 -2
  97. package/src/atoms/island/island.types.ts +8 -4
  98. package/src/atoms/pressable/pressable.tsx +46 -6
  99. package/src/atoms/pressable/pressable.types.ts +5 -3
  100. package/src/atoms/rn-text/rn-text.tsx +6 -3
  101. package/src/atoms/rn-text/rn-text.types.ts +15 -9
  102. package/src/atoms/skeleton/skeleton.tsx +4 -4
  103. package/src/atoms/skeleton/skeleton.types.ts +8 -4
  104. package/src/atoms/text/text.android.tsx +7 -1
  105. package/src/atoms/text/text.ios.tsx +14 -3
  106. package/src/atoms/text/text.types.ts +7 -2
  107. package/src/molecules/collapsible/collapsible.android.tsx +47 -7
  108. package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -6
  109. package/src/molecules/pager/pager.android.tsx +4 -1
  110. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +103 -10
  111. package/src/molecules/scroll-edge-container/scroll-edge-container.types.ts +6 -4
  112. package/src/molecules/stepper/stepper.ios.tsx +37 -1
  113. package/src/molecules/stepper/stepper.types.ts +1 -4
  114. package/src/ripple-clip.ts +10 -1
  115. package/lib/module/atoms/activity-indicator/activity-indicator.js +0 -14
  116. package/lib/module/atoms/activity-indicator/activity-indicator.js.map +0 -1
  117. package/src/atoms/activity-indicator/activity-indicator.tsx +0 -9
@@ -1,25 +1,65 @@
1
- import {AnimatedVisibility, Column, Text as ComposeText, Row} from '@expo/ui/jetpack-compose'
2
- import {clickable} from '@expo/ui/jetpack-compose/modifiers'
3
- import {useAppTheme} from '@hzblj/kara-theme'
1
+ import {
2
+ AnimatedVisibility,
3
+ Box,
4
+ Column,
5
+ Text as ComposeText,
6
+ EnterTransition,
7
+ ExitTransition,
8
+ Row,
9
+ } from '@expo/ui/jetpack-compose'
10
+ import {
11
+ animated,
12
+ clickable,
13
+ defaultMinSize,
14
+ fillMaxWidth,
15
+ graphicsLayer,
16
+ tween,
17
+ } from '@expo/ui/jetpack-compose/modifiers'
18
+ import {duration, useAppTheme} from '@hzblj/kara-theme'
4
19
  import {useMemo} from 'react'
5
20
  import {Glyph} from '../../glyph'
6
21
  import {useHostGuard} from '../../host-context'
7
22
  import type {CollapsibleComponent, CollapsibleProps} from './collapsible.types'
8
23
 
24
+ /** Material's standard easing, at the length a disclosure takes. */
25
+ const TURN = tween({durationMillis: duration.d200, easing: 'fastOutSlowIn'})
26
+
27
+ /** Down from the row, fading in — Compose's default grows from a corner, sideways as well as down. */
28
+ const ENTER = EnterTransition.expandVertically().plus(EnterTransition.fadeIn())
29
+ const EXIT = ExitTransition.shrinkVertically().plus(ExitTransition.fadeOut())
30
+
31
+ const WIDE = [fillMaxWidth()]
32
+
33
+ /** Material's least touch target. */
34
+ const ROW_HEIGHT = 48
35
+
36
+ /**
37
+ * A pressable row and an animated reveal: the content expands down from the row, and the chevron turns a quarter
38
+ * toward it, as a Material disclosure does — it used to jump between two glyphs. The row spans its parent, the chevron
39
+ * at the far end, and is never shorter than a touch target.
40
+ */
9
41
  export const Collapsible: CollapsibleComponent = ({children, expanded, label, onExpandedChange}: CollapsibleProps) => {
10
42
  useHostGuard('Collapsible')
11
43
  const theme = useAppTheme()
12
- const modifiers = useMemo(() => [clickable(() => onExpandedChange(!expanded))], [expanded, onExpandedChange])
44
+ const modifiers = useMemo(
45
+ () => [fillMaxWidth(), defaultMinSize({minHeight: ROW_HEIGHT}), clickable(() => onExpandedChange(!expanded))],
46
+ [expanded, onExpandedChange]
47
+ )
48
+ const turn = useMemo(() => [graphicsLayer({rotationZ: animated(expanded ? 90 : 0, TURN)})], [expanded])
13
49
 
14
50
  return (
15
- <Column>
51
+ <Column modifiers={WIDE}>
16
52
  <Row horizontalArrangement="spaceBetween" modifiers={modifiers} verticalAlignment="center">
17
53
  <ComposeText color={theme.colorString('content.primary')} style={theme.typography.body}>
18
54
  {label}
19
55
  </ComposeText>
20
- <Glyph name={expanded ? 'chevronDown' : 'chevronRight'} size="s20" />
56
+ <Box modifiers={turn}>
57
+ <Glyph name="chevronRight" size="s20" />
58
+ </Box>
21
59
  </Row>
22
- <AnimatedVisibility visible={expanded}>{children}</AnimatedVisibility>
60
+ <AnimatedVisibility enterTransition={ENTER} exitTransition={EXIT} visible={expanded}>
61
+ {children}
62
+ </AnimatedVisibility>
23
63
  </Column>
24
64
  )
25
65
  }
@@ -2,17 +2,12 @@ import {iconResourceName} from '@hzblj/kara-native'
2
2
  import {TopAppBar, type TopAppBarItem} from '@hzblj/kara-native/jetpack-compose'
3
3
  import {useAppTheme} from '@hzblj/kara-theme'
4
4
  import {useMemo} from 'react'
5
- import {StyleSheet} from 'react-native'
6
5
  import {Island} from '../../atoms/island'
7
6
  import type {NavigationBarComponent, NavigationBarItem, NavigationBarProps} from './navigation-bar.types'
8
7
 
9
8
  /** Material's small top app bar. */
10
9
  const BAR_HEIGHT = 64
11
10
 
12
- const styles = StyleSheet.create({
13
- bar: {height: BAR_HEIGHT, width: '100%'},
14
- })
15
-
16
11
  const DEFAULT_LABELS = {back: 'Back', close: 'Close'} as const
17
12
 
18
13
  const toNativeItem = ({onPress, testID, ...item}: NavigationBarItem): TopAppBarItem =>
@@ -38,7 +33,7 @@ export const NavigationBar: NavigationBarComponent = ({
38
33
  const nativeTrailingItems = useMemo(() => trailingItems?.map(toNativeItem), [trailingItems])
39
34
 
40
35
  return (
41
- <Island matchContents={false} style={styles.bar} testID={testID}>
36
+ <Island h={BAR_HEIGHT} matchContents={false} testID={testID} w="100%">
42
37
  <TopAppBar
43
38
  fontFamily={font.fontFamily}
44
39
  fontSize={font.fontSize}
@@ -4,15 +4,17 @@ import {spacing} from '@hzblj/kara-theme'
4
4
  import {Children, useCallback, useEffect, useMemo, useRef, useState} from 'react'
5
5
 
6
6
  import {HStack} from '../../atoms/h-stack'
7
+ import {Spacer} from '../../atoms/spacer'
7
8
  import {VStack} from '../../atoms/v-stack'
8
9
  import {useHostGuard} from '../../host-context'
9
10
  import type {PagerComponent, PagerProps} from './pager.types'
10
11
 
11
12
  const DOT_SIZE = 6
12
13
 
13
- /** Compose ships no page indicator, so the dots are composed out of atoms. */
14
+ /** Compose ships no page indicator, so the dots are composed out of atoms — centred between two spacers. */
14
15
  const PagerDots = ({count, current}: {count: number; current: number}) => (
15
16
  <HStack align="center" py="s12" spacing="s6" wide>
17
+ <Spacer />
16
18
  {Array.from({length: count}, (_, index) => (
17
19
  <VStack
18
20
  bg={index === current ? 'content.primary' : 'fill.tertiary'}
@@ -22,6 +24,7 @@ const PagerDots = ({count, current}: {count: number; current: number}) => (
22
24
  w={DOT_SIZE}
23
25
  />
24
26
  ))}
27
+ <Spacer />
25
28
  </HStack>
26
29
  )
27
30
 
@@ -1,9 +1,49 @@
1
- import {Box} from '../../atoms/box'
1
+ import {duration, opacity} from '@hzblj/kara-theme'
2
+ import {cloneElement, type ReactNode, useCallback, useRef} from 'react'
3
+ import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'
4
+ import {type SharedValue, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
5
+
6
+ import {ABox, Box} from '../../atoms/box'
2
7
  import type {ScrollEdgeContainerComponent, ScrollEdgeContainerProps} from './scroll-edge-container.types'
3
8
 
9
+ /** Less than a pixel scrolled is not scrolled — a fling settles a fraction away from the edge. */
10
+ const EDGE_SLOP = 0.5
11
+
12
+ const FILL = {duration: duration.d200}
13
+
14
+ type Bar = {children: ReactNode; filled: SharedValue<number>}
15
+
16
+ /** A bar, and behind it the container colour it fills with — faded in and out, not switched. */
17
+ const EdgeBar = ({children, filled}: Bar) => {
18
+ const fill = useAnimatedStyle(() => ({opacity: filled.value}), [])
19
+
20
+ if (!children) {
21
+ return null
22
+ }
23
+
24
+ return (
25
+ <Box>
26
+ <ABox
27
+ bg="surface.secondary"
28
+ bottom={0}
29
+ left={0}
30
+ pointerEvents="none"
31
+ position="absolute"
32
+ right={0}
33
+ style={fill}
34
+ top={0}
35
+ />
36
+ {children}
37
+ </Box>
38
+ )
39
+ }
40
+
4
41
  /**
5
- * Android has no scroll edge effect, and a React Native scroll view there has no `contentInset` to slide content under
6
- * a bar with — so the bars stack above and below the scroll view instead of over it. Give them a background.
42
+ * Material's take on the scroll edge: a React Native scroll view has no `contentInset` to slide content under a bar on
43
+ * Android, so the bars stack above and below it, and do what a Material top app bar does on scroll — transparent at
44
+ * rest, filled with `surface.secondary` (the scrolled container colour, `surfaceContainer`) once content has scrolled
45
+ * under it. The bottom bar mirrors it: filled while there is more content below. The scroll view keeps its own
46
+ * `onScroll`, `onLayout` and `onContentSizeChange`.
7
47
  */
8
48
  export const ScrollEdgeContainer: ScrollEdgeContainerComponent = ({
9
49
  automaticInsets: _automaticInsets,
@@ -13,10 +53,63 @@ export const ScrollEdgeContainer: ScrollEdgeContainerComponent = ({
13
53
  testID,
14
54
  topBar,
15
55
  ...sx
16
- }: ScrollEdgeContainerProps) => (
17
- <Box {...sx} testID={testID}>
18
- {topBar}
19
- <Box flex={1}>{children}</Box>
20
- {bottomBar}
21
- </Box>
22
- )
56
+ }: ScrollEdgeContainerProps) => {
57
+ const topFilled = useSharedValue<number>(opacity.o0)
58
+ const bottomFilled = useSharedValue<number>(opacity.o0)
59
+ const metrics = useRef({content: 0, offset: 0, viewport: 0})
60
+ const state = useRef({bottom: false, top: false})
61
+ const {flex, onContentSizeChange, onLayout, onScroll, scrollEventThrottle} = children.props as {
62
+ flex?: number
63
+ onContentSizeChange?: (width: number, height: number) => void
64
+ onLayout?: (event: LayoutChangeEvent) => void
65
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
66
+ scrollEventThrottle?: number
67
+ }
68
+
69
+ // Only a change of state animates; a scroll event restating the same one would restart the fade.
70
+ const update = useCallback(() => {
71
+ const {content, offset, viewport} = metrics.current
72
+ const top = offset > EDGE_SLOP
73
+ const bottom = viewport > 0 && offset + viewport < content - EDGE_SLOP
74
+
75
+ if (top !== state.current.top) {
76
+ state.current.top = top
77
+ topFilled.value = withTiming(top ? opacity.o100 : opacity.o0, FILL)
78
+ }
79
+
80
+ if (bottom !== state.current.bottom) {
81
+ state.current.bottom = bottom
82
+ bottomFilled.value = withTiming(bottom ? opacity.o100 : opacity.o0, FILL)
83
+ }
84
+ }, [bottomFilled, topFilled])
85
+
86
+ // `flex` so the scroll view takes the room between the bars — left to its content, it grew to the content's height,
87
+ // ran under the bottom bar and had nothing to scroll.
88
+ const scrollable = cloneElement(children, {
89
+ flex: flex ?? 1,
90
+ onContentSizeChange: (width: number, height: number) => {
91
+ metrics.current.content = height
92
+ update()
93
+ onContentSizeChange?.(width, height)
94
+ },
95
+ onLayout: (event: LayoutChangeEvent) => {
96
+ metrics.current.viewport = event.nativeEvent.layout.height
97
+ update()
98
+ onLayout?.(event)
99
+ },
100
+ onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
101
+ metrics.current.offset = event.nativeEvent.contentOffset.y
102
+ update()
103
+ onScroll?.(event)
104
+ },
105
+ scrollEventThrottle: scrollEventThrottle ?? 16,
106
+ } as Partial<typeof children.props>)
107
+
108
+ return (
109
+ <Box {...sx} testID={testID}>
110
+ <EdgeBar filled={topFilled}>{topBar}</EdgeBar>
111
+ <Box flex={1}>{scrollable}</Box>
112
+ <EdgeBar filled={bottomFilled}>{bottomBar}</EdgeBar>
113
+ </Box>
114
+ )
115
+ }
@@ -5,8 +5,10 @@ import type {ReactElement, ReactNode} from 'react'
5
5
  export type {ScrollEdgeEffect, ScrollEdgeScrollableProps} from '@hzblj/kara-native/swift-ui'
6
6
 
7
7
  /**
8
- * Bars over one React Native scroll view, with the iOS 26 scroll edge effect under them — the system's progressive
9
- * blur where content scrolls past a bar. The React Native tier: it needs no host.
8
+ * Bars over one React Native scroll view, with each platform's scroll edge under them — on iOS 26 the system's
9
+ * progressive blur where content scrolls past a bar; on Android a Material top app bar's behaviour, the bars stacked
10
+ * around the scroll view and filling with `surface.secondary` once content scrolls under them. The React Native tier:
11
+ * it needs no host.
10
12
  *
11
13
  * A screen-level container: wrap the screen's scroll view with it. Mounted inside a cell of a virtualized list on iOS
12
14
  * it draws only its bottom bar — the hosted scroll view and the top bar stay blank.
@@ -17,7 +19,7 @@ export type ScrollEdgeContainerProps = ResponsiveViewSx & {
17
19
  * content begins below the top bar and scrolls under it. On by default.
18
20
  */
19
21
  automaticInsets?: boolean
20
- /** Laid over the scroll view's bottom edge on iOS; stacked below it on Android, where it wants a background. */
22
+ /** Laid over the scroll view's bottom edge on iOS; stacked below it on Android, filled while content continues below. */
21
23
  bottomBar?: ReactNode
22
24
  /** Exactly one React Native scroll view — `RnScrollView`, `RnFlatList`, `RnSectionList`. */
23
25
  children: ReactElement<ScrollEdgeScrollableProps>
@@ -25,7 +27,7 @@ export type ScrollEdgeContainerProps = ResponsiveViewSx & {
25
27
  effect?: ScrollEdgeEffect
26
28
  /** Test identifier, forwarded to the container view. */
27
29
  testID?: string
28
- /** Laid over the scroll view's top edge on iOS; stacked above it on Android, where it wants a background. */
30
+ /** Laid over the scroll view's top edge on iOS; stacked above it on Android, filled once content scrolls under it. */
29
31
  topBar?: ReactNode
30
32
  }
31
33
 
@@ -1,10 +1,46 @@
1
1
  import {Stepper as SwiftUIStepper} from '@expo/ui/swift-ui'
2
+ import {labelsHidden} from '@expo/ui/swift-ui/modifiers'
3
+ import {useRef} from 'react'
2
4
 
5
+ import {HStack} from '../../atoms/h-stack'
6
+ import {Spacer} from '../../atoms/spacer'
7
+ import {Text} from '../../atoms/text'
3
8
  import {useHostGuard} from '../../host-context'
4
9
  import type {StepperComponent, StepperProps} from './stepper.types'
5
10
 
11
+ /** The label stays VoiceOver's, on the control; on screen it is the row's own text. */
12
+ const LABEL_HIDDEN = [labelsHidden()]
13
+
14
+ /**
15
+ * SwiftUI's `Stepper` at the end of a row, the label leading and the value just before the control — the way iOS's own
16
+ * settings show a stepped value. The value rolls to the next one (`numericText`), up or down with the change, in
17
+ * monospaced digits so the row does not jump.
18
+ */
6
19
  export const Stepper: StepperComponent = ({label, max, min, onValueChange, step = 1, value}: StepperProps) => {
7
20
  useHostGuard('Stepper')
21
+ const previous = useRef(value)
22
+ const countsDown = value < previous.current
23
+ previous.current = value
8
24
 
9
- return <SwiftUIStepper label={label} max={max} min={min} onValueChange={onValueChange} step={step} value={value} />
25
+ return (
26
+ <HStack align="center" spacing="s12" wide>
27
+ <Text>{label}</Text>
28
+ <Spacer />
29
+ <Text
30
+ color="content.secondary"
31
+ ios={{animation: {value}, contentTransition: {countsDown, type: 'numericText'}, monospacedDigit: true}}
32
+ >
33
+ {String(value)}
34
+ </Text>
35
+ <SwiftUIStepper
36
+ label={label}
37
+ max={max}
38
+ min={min}
39
+ modifiers={LABEL_HIDDEN}
40
+ onValueChange={onValueChange}
41
+ step={step}
42
+ value={value}
43
+ />
44
+ </HStack>
45
+ )
10
46
  }
@@ -5,10 +5,7 @@ import type {ReactNode} from 'react'
5
5
  * two icon buttons in a row. A native atom — it renders inside `<Screen>` or `<Island>`.
6
6
  */
7
7
  export type StepperProps = {
8
- /**
9
- * The leading text. iOS shows only the label, not the value — put the value in it if it should be seen. Android
10
- * shows the value between its buttons.
11
- */
8
+ /** The leading text. The value is shown too — before the control on iOS, between the buttons on Android. */
12
9
  label: string
13
10
  /** The highest value. When unset, Android has no upper limit and iOS stops at `100`. */
14
11
  max?: number
@@ -23,6 +23,9 @@ const CONTENT_KEYS = new Set<string>([
23
23
 
24
24
  const FILL: ViewStyle = {flexGrow: 1}
25
25
 
26
+ /** What gives the outer view a height of its own, rather than its content's. */
27
+ const SIZING_KEYS = ['aspectRatio', 'flex', 'flexBasis', 'flexGrow', 'height', 'minHeight'] as const
28
+
26
29
  /**
27
30
  * Splits a resolved style for a pressable with an Android ripple. The ripple is drawn as the pressable's foreground,
28
31
  * which its own `overflow: hidden` does not clip — so it would spill past rounded corners. A parent clips its
@@ -43,5 +46,11 @@ export const splitForRipple = (style: ViewStyle | undefined): {inner: ViewStyle[
43
46
  }
44
47
  }
45
48
 
46
- return {inner: [FILL, inner as ViewStyle], outer: outer as ViewStyle}
49
+ // Only a sized outer view needs the pressable stretched to it. Sized by its content it must not be: Yoga grows a
50
+ // `flexGrow` child of a fit-content parent to the most it may take — inside a row centred on its cross axis, the
51
+ // pressable would stretch to the row's full height and leave its content at the top.
52
+ const sized =
53
+ SIZING_KEYS.some(key => flat[key] !== undefined) || (flat.top !== undefined && flat.bottom !== undefined)
54
+
55
+ return {inner: sized ? [FILL, inner as ViewStyle] : [inner as ViewStyle], outer: outer as ViewStyle}
47
56
  }
@@ -1,14 +0,0 @@
1
- "use strict";
2
-
3
- import { useAppTheme } from '@hzblj/kara-theme';
4
- import { ActivityIndicator as RNActivityIndicator } from 'react-native';
5
- import { jsx as _jsx } from "react/jsx-runtime";
6
- export const ActivityIndicator = props => {
7
- const theme = useAppTheme();
8
- return /*#__PURE__*/_jsx(RNActivityIndicator, {
9
- color: theme.color('fill.accent.primary'),
10
- size: "small",
11
- ...props
12
- });
13
- };
14
- //# sourceMappingURL=activity-indicator.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["useAppTheme","ActivityIndicator","RNActivityIndicator","jsx","_jsx","props","theme","color","size"],"sourceRoot":"../../../../src","sources":["atoms/activity-indicator/activity-indicator.tsx"],"mappings":";;AAAA,SAAQA,WAAW,QAAO,mBAAmB;AAE7C,SAAqCC,iBAAiB,IAAIC,mBAAmB,QAAO,cAAc;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAElG,OAAO,MAAMH,iBAA6C,GAAGI,KAAK,IAAI;EACpE,MAAMC,KAAK,GAAGN,WAAW,CAAC,CAAC;EAE3B,oBAAOI,IAAA,CAACF,mBAAmB;IAACK,KAAK,EAAED,KAAK,CAACC,KAAK,CAAC,qBAAqB,CAAE;IAACC,IAAI,EAAC,OAAO;IAAA,GAAKH;EAAK,CAAG,CAAC;AACnG,CAAC","ignoreList":[]}
@@ -1,9 +0,0 @@
1
- import {useAppTheme} from '@hzblj/kara-theme'
2
- import type {FC} from 'react'
3
- import {type ActivityIndicatorProps, ActivityIndicator as RNActivityIndicator} from 'react-native'
4
-
5
- export const ActivityIndicator: FC<ActivityIndicatorProps> = props => {
6
- const theme = useAppTheme()
7
-
8
- return <RNActivityIndicator color={theme.color('fill.accent.primary')} size="small" {...props} />
9
- }