@hzblj/kara-components 0.11.0 → 0.12.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 (55) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/module/atoms/box/box.js +3 -0
  3. package/lib/module/atoms/box/box.js.map +1 -1
  4. package/lib/module/atoms/box/index.js +0 -1
  5. package/lib/module/atoms/box/index.js.map +1 -1
  6. package/lib/module/atoms/index.js +1 -0
  7. package/lib/module/atoms/index.js.map +1 -1
  8. package/lib/module/atoms/pressable/pressable.js +62 -15
  9. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  10. package/lib/module/atoms/rn-text/rn-text.js +66 -26
  11. package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
  12. package/lib/module/atoms/skeleton/skeleton.js +3 -3
  13. package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
  14. package/lib/module/atoms/stagger/index.js +5 -0
  15. package/lib/module/atoms/stagger/index.js.map +1 -0
  16. package/lib/module/atoms/stagger/stagger.js +102 -0
  17. package/lib/module/atoms/stagger/stagger.js.map +1 -0
  18. package/lib/module/atoms/stagger/stagger.types.js +4 -0
  19. package/lib/module/atoms/stagger/stagger.types.js.map +1 -0
  20. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +3 -3
  21. package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
  22. package/lib/module/organisms/snackbar/snackbar.js +32 -20
  23. package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
  24. package/lib/typescript/atoms/box/box.d.ts +3 -0
  25. package/lib/typescript/atoms/box/index.d.ts +0 -1
  26. package/lib/typescript/atoms/index.d.ts +1 -0
  27. package/lib/typescript/atoms/pressable/pressable.types.d.ts +4 -3
  28. package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -4
  29. package/lib/typescript/atoms/stagger/index.d.ts +2 -0
  30. package/lib/typescript/atoms/stagger/stagger.d.ts +2 -0
  31. package/lib/typescript/atoms/stagger/stagger.types.d.ts +32 -0
  32. package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +6 -8
  33. package/package.json +4 -4
  34. package/src/atoms/box/box.tsx +3 -0
  35. package/src/atoms/box/index.ts +0 -1
  36. package/src/atoms/index.ts +1 -0
  37. package/src/atoms/pressable/pressable.tsx +162 -106
  38. package/src/atoms/pressable/pressable.types.ts +4 -2
  39. package/src/atoms/rn-text/rn-text.tsx +101 -32
  40. package/src/atoms/rn-text/rn-text.types.ts +7 -3
  41. package/src/atoms/skeleton/skeleton.tsx +3 -3
  42. package/src/atoms/stagger/index.ts +2 -0
  43. package/src/atoms/stagger/stagger.tsx +98 -0
  44. package/src/atoms/stagger/stagger.types.ts +35 -0
  45. package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +3 -3
  46. package/src/organisms/snackbar/snackbar.tsx +20 -40
  47. package/src/organisms/snackbar/snackbar.types.ts +6 -8
  48. package/lib/module/atoms/box/a-box.js +0 -6
  49. package/lib/module/atoms/box/a-box.js.map +0 -1
  50. package/lib/module/motion.js +0 -19
  51. package/lib/module/motion.js.map +0 -1
  52. package/lib/typescript/atoms/box/a-box.d.ts +0 -49
  53. package/lib/typescript/motion.d.ts +0 -8
  54. package/src/atoms/box/a-box.ts +0 -5
  55. package/src/motion.ts +0 -13
@@ -1,36 +1,105 @@
1
- import {useSplitSx} from '@hzblj/kara-sx'
2
- import {MAX_FONT_SCALE, useAppTheme} from '@hzblj/kara-theme'
3
- import {Text as RNText, type TextStyle, useWindowDimensions} from 'react-native'
1
+ import {
2
+ isAnimatedStyle,
3
+ markMotionComponent,
4
+ type SxMotion,
5
+ themeKey,
6
+ useAnimatedLatch,
7
+ useSplitSxMotion,
8
+ } from '@hzblj/kara-sx'
9
+ import {
10
+ type ColorToken,
11
+ MAX_FONT_SCALE,
12
+ type TextVariant,
13
+ type Theme,
14
+ useAppTheme,
15
+ useFontScale,
16
+ } from '@hzblj/kara-theme'
17
+ import {Platform, Text as RNText, type StyleProp, type TextStyle} from 'react-native'
18
+ import Animated from 'react-native-reanimated'
4
19
 
5
20
  import type {RnTextComponent, RnTextProps} from './rn-text.types'
6
21
 
7
- export const RnText: RnTextComponent = ({
8
- align,
9
- color = 'content.primary',
10
- variant = 'body',
11
- ...props
12
- }: RnTextProps) => {
13
- const theme = useAppTheme()
14
- // A view style, which a text accepts whole — only React Native's types disagree, on `userSelect`'s width.
15
- const [sx, rest] = useSplitSx(props) as [TextStyle, Record<string, unknown>]
16
- const {fontScale} = useWindowDimensions()
17
- const style = theme.typography[variant]
18
-
19
- /**
20
- * React Native scales `fontSize` for the user's setting but leaves `lineHeight` alone, so a grown glyph ends up in
21
- * an ungrown line and clips. Scaling it here by the same factor keeps the ratio the variant was designed at — and
22
- * by the same ceiling `maxFontSizeMultiplier` caps the glyph with, so the two cannot disagree.
23
- *
24
- * The native tiers need none of this: SwiftUI's `lineSpacing` is extra space *on top of* the font's own line
25
- * height, which scales with it, and Compose reads `lineHeight` in `sp`.
26
- */
27
- const lineHeight = style.lineHeight * Math.min(fontScale, MAX_FONT_SCALE)
28
-
29
- return (
30
- <RNText
31
- maxFontSizeMultiplier={MAX_FONT_SCALE}
32
- style={[style, sx, {color: theme.color(color), lineHeight, textAlign: align}]}
33
- {...rest}
34
- />
35
- )
22
+ /**
23
+ * A hundredth of a point off every line on iOS, which no one can see and which keeps the last line on screen. React
24
+ * Native measures a paragraph and rounds the height up to the pixel grid, then draws it into the frame Yoga laid out,
25
+ * and that frame can come back a hair short of the exact height (`53.9998` for three 18 pt lines, depending on where
26
+ * the text sits on the page). With `lineHeight` fixed, the lines need exactly 54 and the text container drops the last
27
+ * one, cutting the line before it off mid-word. Lines a hair shorter leave the rounding room to absorb the error.
28
+ */
29
+ const LINE_HEIGHT_SLACK = Platform.OS === 'ios' ? 0.01 : 0
30
+
31
+ /** Variants, colours and alignments are few, so the text half of the style is shared like the `sx` half. */
32
+ const MAX_TEXT_STYLES = 512
33
+
34
+ const textStyles = new Map<string, TextStyle>()
35
+
36
+ /**
37
+ * The variant at the user's text size, in its colour. React Native scales `fontSize` for the user's setting but leaves
38
+ * `lineHeight` alone, so a grown glyph ends up in an ungrown line and clips. Scaling it here by the same factor keeps
39
+ * the ratio the variant was designed at, and by the same ceiling `maxFontSizeMultiplier` caps the glyph with
40
+ * (`useFontScale` is capped already), so the two cannot disagree.
41
+ *
42
+ * The native tiers need none of this: SwiftUI's `lineSpacing` is extra space *on top of* the font's own line
43
+ * height, which scales with it, and Compose reads `lineHeight` in `sp`.
44
+ */
45
+ const textStyle = (
46
+ theme: Theme,
47
+ variant: TextVariant,
48
+ color: ColorToken,
49
+ align: RnTextProps['align'],
50
+ fontScale: number
51
+ ): TextStyle => {
52
+ const key = `${themeKey(theme)}|${variant}|${color}|${align}|${fontScale}`
53
+ const existing = textStyles.get(key)
54
+
55
+ if (existing) {
56
+ return existing
57
+ }
58
+
59
+ if (textStyles.size >= MAX_TEXT_STYLES) {
60
+ textStyles.clear()
61
+ }
62
+
63
+ const typography = theme.typography[variant]
64
+ const style: TextStyle = {
65
+ ...typography,
66
+ color: theme.color(color),
67
+ lineHeight: typography.lineHeight * fontScale - LINE_HEIGHT_SLACK,
68
+ }
69
+
70
+ if (align) {
71
+ style.textAlign = align
72
+ }
73
+
74
+ textStyles.set(key, style)
75
+
76
+ return style
36
77
  }
78
+
79
+ export const RnText: RnTextComponent = markMotionComponent(
80
+ ({align, color = 'content.primary', style, variant = 'body', ...props}: RnTextProps) => {
81
+ const theme = useAppTheme()
82
+ const fontScale = useFontScale()
83
+ // A view style, which a text accepts whole — only React Native's types disagree, on `userSelect`'s width.
84
+ const [sx, motion, rest] = useSplitSxMotion(props) as [TextStyle, SxMotion, Record<string, unknown>]
85
+ const animated = useAnimatedLatch(motion.animated || isAnimatedStyle(style), 'RnText')
86
+ const text = textStyle(theme, variant, color, align, fontScale)
87
+
88
+ if (!animated) {
89
+ return (
90
+ <RNText maxFontSizeMultiplier={MAX_FONT_SCALE} style={[text, sx, style as StyleProp<TextStyle>]} {...rest} />
91
+ )
92
+ }
93
+
94
+ return (
95
+ <Animated.Text
96
+ entering={motion.entering}
97
+ exiting={motion.exiting}
98
+ layout={motion.layout}
99
+ maxFontSizeMultiplier={MAX_FONT_SCALE}
100
+ style={[text, sx, motion.style, style]}
101
+ {...rest}
102
+ />
103
+ )
104
+ }
105
+ )
@@ -1,19 +1,23 @@
1
- import type {ResponsiveViewSx} from '@hzblj/kara-sx'
1
+ import type {MotionProps, ResponsiveViewSx} from '@hzblj/kara-sx'
2
2
  import type {ColorToken, TextVariant} from '@hzblj/kara-theme'
3
3
  import type {ReactNode} from 'react'
4
- import type {TextProps as RNTextProps} from 'react-native'
4
+ import type {TextProps as RNTextProps, StyleProp, TextStyle} from 'react-native'
5
+ import type {AnimatedStyle} from 'react-native-reanimated'
5
6
 
6
7
  /**
7
8
  * React Native's `Text` in a theme typography variant: the React Native tier, for text outside a native host. It takes
8
9
  * `Box`'s `sx` surface for its own box (margins, flex, padding, a background), all but `align`, which here aligns the
9
- * lines.
10
+ * lines, and the motion props a `Box` takes: `entering="fadeUp"` makes it an `Animated.Text` by itself.
10
11
  */
11
12
  export type RnTextProps = Omit<RNTextProps, 'style'> &
13
+ MotionProps &
12
14
  Omit<ResponsiveViewSx, 'align'> & {
13
15
  /** Horizontal alignment, as React Native's `textAlign`. */
14
16
  align?: 'center' | 'left' | 'right'
15
17
  /** The text's colour. `content.primary` by default. */
16
18
  color?: ColorToken
19
+ /** Goes last, over the variant and the `sx`: an animated style from `useAnimatedStyle`, which it renders through Reanimated for. */
20
+ style?: StyleProp<AnimatedStyle<TextStyle>>
17
21
  /** The typography variant. `body` by default. */
18
22
  variant?: TextVariant
19
23
  }
@@ -1,9 +1,9 @@
1
+ import {timing} from '@hzblj/kara-sx'
1
2
  import {duration, opacity, useReducedMotion} from '@hzblj/kara-theme'
2
3
  import {useEffect} from 'react'
3
4
  import {cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated'
4
5
 
5
- import {timing} from '../../motion'
6
- import {ABox} from '../box'
6
+ import {Box} from '../box'
7
7
  import type {SkeletonComponent, SkeletonProps} from './skeleton.types'
8
8
 
9
9
  /** A breath, not a transition, so it sits off the scale: `slow` twice each way, at constant speed. */
@@ -28,7 +28,7 @@ export const Skeleton: SkeletonComponent = ({borderRadius = 'small', h, testID,
28
28
  const rStyle = useAnimatedStyle(() => ({opacity: pulse.value}), [])
29
29
 
30
30
  return (
31
- <ABox
31
+ <Box
32
32
  aria-hidden
33
33
  bg="fill.quaternary"
34
34
  borderRadius={borderRadius}
@@ -0,0 +1,2 @@
1
+ export * from './stagger'
2
+ export * from './stagger.types'
@@ -0,0 +1,98 @@
1
+ import {isMotionComponent, type MotionPresetConfig, type SxEntryExit, type SxLayout} from '@hzblj/kara-sx'
2
+ import {duration} from '@hzblj/kara-theme'
3
+ import {Children, cloneElement, isValidElement, type Key, type ReactElement, useLayoutEffect, useRef} from 'react'
4
+
5
+ import {Box} from '../box'
6
+ import type {StaggerComponent, StaggerMotion, StaggerProps} from './stagger.types'
7
+
8
+ type MotionChildProps = {entering?: SxEntryExit; exiting?: SxEntryExit; layout?: SxLayout}
9
+
10
+ const asConfig = (motion: StaggerMotion): MotionPresetConfig => (typeof motion === 'string' ? {preset: motion} : motion)
11
+
12
+ /**
13
+ * The stagger's preset, or the child's own with the delay added: a child that names its look keeps it and only takes
14
+ * its turn. Reanimated's own builder is the child's, untouched, since adding a delay to one mutates it.
15
+ */
16
+ const withDelay = (own: SxEntryExit | undefined, fallback: MotionPresetConfig, delay: number): SxEntryExit => {
17
+ if (own === undefined) {
18
+ return {...fallback, delay: (fallback.delay ?? 0) + delay}
19
+ }
20
+
21
+ if (typeof own === 'string') {
22
+ return {delay, preset: own}
23
+ }
24
+
25
+ return typeof own === 'object' && own !== null && 'preset' in own ? {...own, delay: (own.delay ?? 0) + delay} : own
26
+ }
27
+
28
+ export const Stagger: StaggerComponent = ({
29
+ children,
30
+ entering = 'fadeUp',
31
+ exiting,
32
+ from = 'start',
33
+ interval = 'stagger',
34
+ layout,
35
+ testID,
36
+ ...sx
37
+ }: StaggerProps) => {
38
+ // The children that have arrived already. Only the ones new in this render take turns, so a list that grows by a
39
+ // page staggers the page, and an item added on its own arrives at once instead of after everything above it.
40
+ const arrived = useRef(new Set<Key>())
41
+ const items = Children.toArray(children)
42
+ const turns = new Map<Key, number>()
43
+
44
+ for (const item of items) {
45
+ if (isValidElement(item) && item.key !== null && !arrived.current.has(item.key)) {
46
+ turns.set(item.key, turns.size)
47
+ }
48
+ }
49
+
50
+ // Marked once committed: a render React throws away must not use up a child's turn.
51
+ useLayoutEffect(() => {
52
+ for (const key of turns.keys()) {
53
+ arrived.current.add(key)
54
+ }
55
+ })
56
+
57
+ const step = duration[interval]
58
+ const enter = asConfig(entering)
59
+ const leave = exiting === undefined ? undefined : asConfig(exiting)
60
+
61
+ const delayOf = (key: Key | null) => {
62
+ const turn = key === null ? undefined : turns.get(key)
63
+
64
+ if (turn === undefined) {
65
+ return 0
66
+ }
67
+
68
+ return (from === 'start' ? turn : turns.size - 1 - turn) * step
69
+ }
70
+
71
+ return (
72
+ // With `layout` the container moves and resizes with its children, so what holds it can follow.
73
+ <Box layout={layout} testID={testID} {...sx}>
74
+ {items.map(item => {
75
+ if (!isValidElement<MotionChildProps>(item)) {
76
+ return item
77
+ }
78
+
79
+ const delay = delayOf(item.key)
80
+
81
+ // A component that takes the motion props gets them directly, with no view added around it.
82
+ if (isMotionComponent(item.type)) {
83
+ return cloneElement(item as ReactElement<MotionChildProps>, {
84
+ entering: withDelay(item.props.entering, enter, delay),
85
+ exiting: item.props.exiting ?? leave,
86
+ layout: item.props.layout ?? layout,
87
+ })
88
+ }
89
+
90
+ return (
91
+ <Box entering={withDelay(undefined, enter, delay)} exiting={leave} key={item.key} layout={layout}>
92
+ {item}
93
+ </Box>
94
+ )
95
+ })}
96
+ </Box>
97
+ )
98
+ }
@@ -0,0 +1,35 @@
1
+ import type {MotionPreset, MotionPresetConfig, ResponsiveViewSx} from '@hzblj/kara-sx'
2
+ import type {DurationSize, SpringName} from '@hzblj/kara-theme'
3
+ import type {ReactNode} from 'react'
4
+
5
+ /** A preset by name, or one adjusted: `{preset: 'fadeUp', duration: 'slow'}`. Its `delay` is where the first one starts. */
6
+ export type StaggerMotion = MotionPreset | MotionPresetConfig
7
+
8
+ /**
9
+ * A `Box` whose children arrive one after another: each takes `entering` a step later than the one before. Children
10
+ * that arrive together are staggered among themselves, the first ones as much as a later batch (the next page of a
11
+ * list), while a single child added later arrives at once. A `Box`, `RnText` or `Pressable` child takes the motion
12
+ * itself; any other child is wrapped in a `Box` that does. With motion reduced every child fades, still one after
13
+ * another. The React Native tier: it needs no host.
14
+ */
15
+ export type StaggerProps = ResponsiveViewSx & {
16
+ /** What arrives. A child with an `entering` preset of its own keeps it and only takes the delay. */
17
+ children?: ReactNode
18
+ /** How each child arrives. `fadeUp` by default. */
19
+ entering?: StaggerMotion
20
+ /** How a child leaves when it is removed. They leave together, not one after another; none by default. */
21
+ exiting?: StaggerMotion
22
+ /** Which end arrives first. `start` by default. */
23
+ from?: 'end' | 'start'
24
+ /** The step between one child and the next, a duration token. `stagger` (40 ms) by default, `micro` for a slower one. */
25
+ interval?: DurationSize
26
+ /**
27
+ * Moves the other children into place when one arrives or leaves, and the stagger itself to its new size: `true` on
28
+ * the `smooth` spring, or a spring token. Off by default. Give what sits after it `layout` too, so it follows.
29
+ */
30
+ layout?: boolean | SpringName
31
+ /** Test identifier, forwarded to the view. */
32
+ testID?: string
33
+ }
34
+
35
+ export type StaggerComponent = (props: StaggerProps) => ReactNode
@@ -1,10 +1,10 @@
1
+ import {timing} from '@hzblj/kara-sx'
1
2
  import {opacity} from '@hzblj/kara-theme'
2
3
  import {cloneElement, type ReactNode, useCallback, useRef} from 'react'
3
4
  import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'
4
5
  import {type SharedValue, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
5
6
 
6
- import {ABox, Box} from '../../atoms/box'
7
- import {timing} from '../../motion'
7
+ import {Box} from '../../atoms/box'
8
8
  import type {ScrollEdgeContainerComponent, ScrollEdgeContainerProps} from './scroll-edge-container.types'
9
9
 
10
10
  /** Less than a pixel scrolled is not scrolled — a fling settles a fraction away from the edge. */
@@ -24,7 +24,7 @@ const EdgeBar = ({children, filled}: Bar) => {
24
24
 
25
25
  return (
26
26
  <Box>
27
- <ABox
27
+ <Box
28
28
  bg="surface.secondary"
29
29
  bottom={0}
30
30
  left={0}
@@ -1,24 +1,13 @@
1
- import {duration as durations, easing, spacing, useReducedMotion} from '@hzblj/kara-theme'
1
+ import {type SxEntryExit, timing} from '@hzblj/kara-sx'
2
+ import {spacing, useReducedMotion} from '@hzblj/kara-theme'
2
3
  import {useEffect, useRef} from 'react'
3
4
  import {AccessibilityInfo, Platform} from 'react-native'
4
- import {
5
- Easing,
6
- type EntryExitAnimationFunction,
7
- FadeIn,
8
- FadeInDown,
9
- FadeInUp,
10
- FadeOut,
11
- FadeOutDown,
12
- FadeOutUp,
13
- ReduceMotion,
14
- withTiming,
15
- } from 'react-native-reanimated'
16
-
17
- import {ABox, Box} from '../../atoms/box'
5
+ import {type EntryExitAnimationFunction, withTiming} from 'react-native-reanimated'
6
+
7
+ import {Box} from '../../atoms/box'
18
8
  import {Pressable} from '../../atoms/pressable'
19
9
  import {RnText} from '../../atoms/rn-text'
20
10
  import {RnGlyph} from '../../glyph'
21
- import {timing} from '../../motion'
22
11
  import type {SnackbarAnimation, SnackbarComponent, SnackbarDuration, SnackbarProps} from './snackbar.types'
23
12
 
24
13
  /** Material's `SnackbarDuration.Short` and `Long`. */
@@ -29,10 +18,9 @@ const PLATFORM_ANIMATION: SnackbarAnimation = Platform.OS === 'android' ? 'scale
29
18
  /** The close glyph at the label's height: an SF Symbol fills its box, Material's icon sits inside a padded one. */
30
19
  const CLOSE_SIZE = Platform.OS === 'ios' ? 's12' : 's16'
31
20
 
32
- const SMOOTH_OUT = Easing.bezier(...easing.smoothOut)
33
21
  const GROW = timing('quick')
34
22
 
35
- /** Material's snackbar enter: it fades in while it grows from 80 %, in 150 ms. */
23
+ /** Material's snackbar enter: it fades in while it grows from 80 %, in 150 ms. Further than kara's `scale` preset. */
36
24
  const scaleIn: EntryExitAnimationFunction = () => {
37
25
  'worklet'
38
26
 
@@ -42,29 +30,21 @@ const scaleIn: EntryExitAnimationFunction = () => {
42
30
  }
43
31
  }
44
32
 
33
+ type Transition = {entering: SxEntryExit; exiting: SxEntryExit}
34
+
45
35
  /**
46
- * The presets in and out: arriving in `fast`, leaving in `quick`, both on `smoothOut`. `ReduceMotion.Never`, because
47
- * `useReducedMotion` has already picked the fade by then, and a fade is what should still run.
36
+ * kara's motion presets, which arrive in `fast`, leave in `quick` and fade with motion reduced by themselves. A slide
37
+ * travels further than a preset's default: in from the edge it sits on, and back out toward it.
48
38
  */
49
39
  const TRANSITIONS = {
50
- fade: {
51
- entering: FadeIn.duration(durations.fast).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
52
- exiting: FadeOut.duration(durations.quick).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
53
- },
40
+ fade: {entering: 'fade', exiting: 'fade'},
54
41
  // Material's exit is a 75 ms fade: `micro`.
55
- scale: {entering: scaleIn, exiting: FadeOut.duration(durations.micro).reduceMotion(ReduceMotion.Never)},
56
- // Slide: in from the edge it sits on, and back out toward it.
57
- slideBottom: {
58
- entering: FadeInDown.duration(durations.fast).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
59
- exiting: FadeOutDown.duration(durations.quick).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
60
- },
61
- slideTop: {
62
- entering: FadeInUp.duration(durations.fast).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
63
- exiting: FadeOutUp.duration(durations.quick).easing(SMOOTH_OUT).reduceMotion(ReduceMotion.Never),
64
- },
65
- }
42
+ scale: {entering: scaleIn, exiting: {duration: 'micro', preset: 'fade'}},
43
+ slideBottom: {entering: {distance: 's24', preset: 'fadeUp'}, exiting: {distance: 's24', preset: 'fadeDown'}},
44
+ slideTop: {entering: {distance: 's24', preset: 'fadeDown'}, exiting: {distance: 's24', preset: 'fadeUp'}},
45
+ } as const satisfies Record<string, Transition>
66
46
 
67
- const transitionsFor = (animation: SnackbarAnimation, position: 'bottom' | 'top') => {
47
+ const transitionsFor = (animation: SnackbarAnimation, position: 'bottom' | 'top'): Transition => {
68
48
  if (animation === 'slide') {
69
49
  return position === 'top' ? TRANSITIONS.slideTop : TRANSITIONS.slideBottom
70
50
  }
@@ -121,11 +101,11 @@ export const Snackbar: SnackbarComponent = ({
121
101
 
122
102
  const edge = position === 'top' ? {top: spacing[offset]} : {bottom: spacing[offset]}
123
103
  const hasTrailing = Boolean(actionLabel) || dismissible
124
- // With motion reduced it only fades: nothing slides or grows.
125
- const preset = transitionsFor(reduced ? 'fade' : animation, position)
104
+ // The presets fade with motion reduced by themselves; Material's scale is a worklet of its own, so it is picked out.
105
+ const preset = transitionsFor(reduced && animation === 'scale' ? 'fade' : animation, position)
126
106
 
127
107
  return (
128
- <ABox
108
+ <Box
129
109
  align={actionOnNewLine ? 'stretch' : 'center'}
130
110
  bg="surface.high.contrast"
131
111
  borderRadius="medium"
@@ -161,6 +141,6 @@ export const Snackbar: SnackbarComponent = ({
161
141
  ) : null}
162
142
  </Box>
163
143
  ) : null}
164
- </ABox>
144
+ </Box>
165
145
  )
166
146
  }
@@ -1,8 +1,6 @@
1
+ import type {SxEntryExit} from '@hzblj/kara-sx'
1
2
  import type {SpaceSize} from '@hzblj/kara-theme'
2
- import type {ComponentProps, ReactNode} from 'react'
3
- import type Animated from 'react-native-reanimated'
4
-
5
- type LayoutAnimation = ComponentProps<typeof Animated.View>['entering']
3
+ import type {ReactNode} from 'react'
6
4
 
7
5
  /** How it comes and goes. */
8
6
  export type SnackbarAnimation = 'fade' | 'scale' | 'slide'
@@ -33,10 +31,10 @@ export type SnackbarProps = {
33
31
  dismissible?: boolean
34
32
  /** How long it stays up before `onDismiss` is called. `indefinite` by default: it never hides itself. */
35
33
  duration?: SnackbarDuration
36
- /** A Reanimated entering animation of your own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
37
- entering?: LayoutAnimation
38
- /** A Reanimated exiting animation of your own. It wins over `animation`. */
39
- exiting?: LayoutAnimation
34
+ /** How it arrives instead: a motion preset (`'scale'`), or Reanimated's own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
35
+ entering?: SxEntryExit
36
+ /** How it leaves instead: a motion preset, or Reanimated's own. It wins over `animation`. */
37
+ exiting?: SxEntryExit
40
38
  /** The message. */
41
39
  message: string
42
40
  /** Between the snackbar and its edge, a space token. `s24` by default; raise it above a tab bar. */
@@ -1,6 +0,0 @@
1
- "use strict";
2
-
3
- import Animated from 'react-native-reanimated';
4
- import { Box } from "./box.js";
5
- export const ABox = Animated.createAnimatedComponent(Box);
6
- //# sourceMappingURL=a-box.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["Animated","Box","ABox","createAnimatedComponent"],"sourceRoot":"../../../../src","sources":["atoms/box/a-box.ts"],"mappings":";;AAAA,OAAOA,QAAQ,MAAM,yBAAyB;AAE9C,SAAQC,GAAG,QAAO,UAAO;AAEzB,OAAO,MAAMC,IAAI,GAAGF,QAAQ,CAACG,uBAAuB,CAACF,GAAG,CAAC","ignoreList":[]}
@@ -1,19 +0,0 @@
1
- "use strict";
2
-
3
- import { duration, easing } from '@hzblj/kara-theme';
4
- import { Easing, ReduceMotion } from 'react-native-reanimated';
5
-
6
- /**
7
- * A Reanimated timing from the motion tokens, `smoothOut` unless told otherwise. `ReduceMotion.Never`, because a
8
- * component has applied `useReducedMotion` before it animates: what is left (a fade, a dim) is meant to run, and
9
- * Reanimated's own check reads the setting once at launch and never hears of `KaraProvider reduceMotion`.
10
- */
11
- export const timing = (size, curve = 'smoothOut') => {
12
- const [x1, y1, x2, y2] = easing[curve];
13
- return {
14
- duration: duration[size],
15
- easing: Easing.bezier(x1, y1, x2, y2),
16
- reduceMotion: ReduceMotion.Never
17
- };
18
- };
19
- //# sourceMappingURL=motion.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["duration","easing","Easing","ReduceMotion","timing","size","curve","x1","y1","x2","y2","bezier","reduceMotion","Never"],"sourceRoot":"../../src","sources":["motion.ts"],"mappings":";;AAAA,SAA2BA,QAAQ,EAAmBC,MAAM,QAAO,mBAAmB;AACtF,SAAQC,MAAM,EAAEC,YAAY,QAA8B,yBAAyB;;AAEnF;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,MAAM,GAAGA,CAACC,IAAkB,EAAEC,KAAiB,GAAG,WAAW,KAAuB;EAC/F,MAAM,CAACC,EAAE,EAAEC,EAAE,EAAEC,EAAE,EAAEC,EAAE,CAAC,GAAGT,MAAM,CAACK,KAAK,CAAC;EAEtC,OAAO;IAACN,QAAQ,EAAEA,QAAQ,CAACK,IAAI,CAAC;IAAEJ,MAAM,EAAEC,MAAM,CAACS,MAAM,CAACJ,EAAE,EAAEC,EAAE,EAAEC,EAAE,EAAEC,EAAE,CAAC;IAAEE,YAAY,EAAET,YAAY,CAACU;EAAK,CAAC;AAC5G,CAAC","ignoreList":[]}
@@ -1,49 +0,0 @@
1
- export declare const ABox: import("react-native-reanimated/lib/typescript/createAnimatedComponent").AnimatedComponentType<Readonly<Omit<import("react-native").ViewProps & import("react").RefAttributes<import("react-native").View>, "align" | "alignContent" | "alignSelf" | "aspectRatio" | "basis" | "bg" | "blur" | "borderColor" | "borderRadius" | "borderWidth" | "bottom" | "columnGap" | "display" | "flex" | "flexDirection" | "gap" | "grow" | "h" | "inset" | "justify" | "left" | "m" | "maxH" | "maxW" | "mb" | "minH" | "minW" | "ml" | "mr" | "mt" | "mx" | "my" | "offsetX" | "offsetY" | "opacity" | "overflow" | "p" | "pb" | "pl" | "position" | "pr" | "pt" | "px" | "py" | "right" | "rotate" | "rowGap" | "scale" | "shadow" | "shrink" | "style" | "tall" | "top" | "w" | "wide" | "wrap" | "zIndex" | keyof {
2
- borderBottomWidth?: number;
3
- borderLeftWidth?: number;
4
- borderRightWidth?: number;
5
- borderStyle?: import("react-native").ViewStyle['borderStyle'];
6
- borderTopWidth?: number;
7
- } | keyof import("@hzblj/kara-sx").ViewRadiiSx> & Omit<import("@hzblj/kara-sx").ViewSx, "align" | "alignContent" | "alignSelf" | "basis" | "columnGap" | "display" | "flex" | "flexDirection" | "gap" | "grow" | "h" | "justify" | "m" | "maxH" | "maxW" | "mb" | "minH" | "minW" | "ml" | "mr" | "mt" | "mx" | "my" | "p" | "pb" | "pl" | "pr" | "pt" | "px" | "py" | "rowGap" | "shrink" | "tall" | "w" | "wide" | "wrap"> & {
8
- p?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
9
- pb?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
10
- pl?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
11
- pr?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
12
- pt?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
13
- px?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
14
- py?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
15
- tall?: import("@hzblj/kara-sx").Responsive<NonNullable<boolean | undefined>> | undefined;
16
- wide?: import("@hzblj/kara-sx").Responsive<NonNullable<boolean | undefined>> | undefined;
17
- h?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
18
- minH?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
19
- minW?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
20
- w?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
21
- maxH?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
22
- maxW?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
23
- align?: import("@hzblj/kara-sx").Responsive<NonNullable<import("react-native").FlexAlignType | undefined>> | undefined;
24
- alignContent?: import("@hzblj/kara-sx").Responsive<NonNullable<"center" | "flex-end" | "flex-start" | "space-around" | "space-between" | "space-evenly" | "stretch" | undefined>> | undefined;
25
- alignSelf?: import("@hzblj/kara-sx").Responsive<NonNullable<"auto" | import("react-native").FlexAlignType | undefined>> | undefined;
26
- basis?: import("@hzblj/kara-sx").Responsive<NonNullable<import("@hzblj/kara-sx").SxDimension | undefined>> | undefined;
27
- columnGap?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
28
- flex?: import("@hzblj/kara-sx").Responsive<number> | undefined;
29
- flexDirection?: import("@hzblj/kara-sx").Responsive<NonNullable<"column" | "column-reverse" | "row" | "row-reverse" | undefined>> | undefined;
30
- gap?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
31
- grow?: import("@hzblj/kara-sx").Responsive<number> | undefined;
32
- justify?: import("@hzblj/kara-sx").Responsive<NonNullable<"center" | "flex-end" | "flex-start" | "space-around" | "space-between" | "space-evenly" | undefined>> | undefined;
33
- rowGap?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
34
- shrink?: import("@hzblj/kara-sx").Responsive<number> | undefined;
35
- wrap?: import("@hzblj/kara-sx").Responsive<NonNullable<"nowrap" | "wrap" | "wrap-reverse" | undefined>> | undefined;
36
- m?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
37
- mb?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
38
- ml?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
39
- mr?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
40
- mt?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
41
- mx?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
42
- my?: import("@hzblj/kara-sx").Responsive<NonNullable<"s0" | "s1" | "s10" | "s12" | "s16" | "s2" | "s20" | "s24" | "s28" | "s32" | "s36" | "s4" | "s40" | "s48" | "s56" | "s6" | "s64" | "s72" | "s8" | "s80" | "s96" | undefined>> | undefined;
43
- display?: import("@hzblj/kara-sx").Responsive<NonNullable<"contents" | "flex" | "none" | undefined>> | undefined;
44
- } & {
45
- style?: import("react-native").RecursiveArray<import("react-native").TextStyle | import("react-native").ViewStyle | import("react-native").Falsy> | import("react-native").TextStyle | import("react-native").ViewStyle | import("react-native").Falsy;
46
- }>, import("react").MemoExoticComponent<{
47
- ({ style, ...props }: import("@hzblj/kara-sx").SxProps<import("react-native").ViewProps & import("react").RefAttributes<import("react-native").View>>): import("react").JSX.Element;
48
- displayName: string;
49
- }>>;
@@ -1,8 +0,0 @@
1
- import { type DurationSize, type EasingName } from '@hzblj/kara-theme';
2
- import { type WithTimingConfig } from 'react-native-reanimated';
3
- /**
4
- * A Reanimated timing from the motion tokens, `smoothOut` unless told otherwise. `ReduceMotion.Never`, because a
5
- * component has applied `useReducedMotion` before it animates: what is left (a fade, a dim) is meant to run, and
6
- * Reanimated's own check reads the setting once at launch and never hears of `KaraProvider reduceMotion`.
7
- */
8
- export declare const timing: (size: DurationSize, curve?: EasingName) => WithTimingConfig;
@@ -1,5 +0,0 @@
1
- import Animated from 'react-native-reanimated'
2
-
3
- import {Box} from './box'
4
-
5
- export const ABox = Animated.createAnimatedComponent(Box)
package/src/motion.ts DELETED
@@ -1,13 +0,0 @@
1
- import {type DurationSize, duration, type EasingName, easing} from '@hzblj/kara-theme'
2
- import {Easing, ReduceMotion, type WithTimingConfig} from 'react-native-reanimated'
3
-
4
- /**
5
- * A Reanimated timing from the motion tokens, `smoothOut` unless told otherwise. `ReduceMotion.Never`, because a
6
- * component has applied `useReducedMotion` before it animates: what is left (a fade, a dim) is meant to run, and
7
- * Reanimated's own check reads the setting once at launch and never hears of `KaraProvider reduceMotion`.
8
- */
9
- export const timing = (size: DurationSize, curve: EasingName = 'smoothOut'): WithTimingConfig => {
10
- const [x1, y1, x2, y2] = easing[curve]
11
-
12
- return {duration: duration[size], easing: Easing.bezier(x1, y1, x2, y2), reduceMotion: ReduceMotion.Never}
13
- }