@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.
- package/CHANGELOG.md +28 -0
- package/lib/module/atoms/box/box.js +3 -0
- package/lib/module/atoms/box/box.js.map +1 -1
- package/lib/module/atoms/box/index.js +0 -1
- package/lib/module/atoms/box/index.js.map +1 -1
- package/lib/module/atoms/index.js +1 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/atoms/pressable/pressable.js +62 -15
- package/lib/module/atoms/pressable/pressable.js.map +1 -1
- package/lib/module/atoms/rn-text/rn-text.js +66 -26
- package/lib/module/atoms/rn-text/rn-text.js.map +1 -1
- package/lib/module/atoms/skeleton/skeleton.js +3 -3
- package/lib/module/atoms/skeleton/skeleton.js.map +1 -1
- package/lib/module/atoms/stagger/index.js +5 -0
- package/lib/module/atoms/stagger/index.js.map +1 -0
- package/lib/module/atoms/stagger/stagger.js +102 -0
- package/lib/module/atoms/stagger/stagger.js.map +1 -0
- package/lib/module/atoms/stagger/stagger.types.js +4 -0
- package/lib/module/atoms/stagger/stagger.types.js.map +1 -0
- package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js +3 -3
- package/lib/module/molecules/scroll-edge-container/scroll-edge-container.android.js.map +1 -1
- package/lib/module/organisms/snackbar/snackbar.js +32 -20
- package/lib/module/organisms/snackbar/snackbar.js.map +1 -1
- package/lib/typescript/atoms/box/box.d.ts +3 -0
- package/lib/typescript/atoms/box/index.d.ts +0 -1
- package/lib/typescript/atoms/index.d.ts +1 -0
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +4 -3
- package/lib/typescript/atoms/rn-text/rn-text.types.d.ts +7 -4
- package/lib/typescript/atoms/stagger/index.d.ts +2 -0
- package/lib/typescript/atoms/stagger/stagger.d.ts +2 -0
- package/lib/typescript/atoms/stagger/stagger.types.d.ts +32 -0
- package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +6 -8
- package/package.json +4 -4
- package/src/atoms/box/box.tsx +3 -0
- package/src/atoms/box/index.ts +0 -1
- package/src/atoms/index.ts +1 -0
- package/src/atoms/pressable/pressable.tsx +162 -106
- package/src/atoms/pressable/pressable.types.ts +4 -2
- package/src/atoms/rn-text/rn-text.tsx +101 -32
- package/src/atoms/rn-text/rn-text.types.ts +7 -3
- package/src/atoms/skeleton/skeleton.tsx +3 -3
- package/src/atoms/stagger/index.ts +2 -0
- package/src/atoms/stagger/stagger.tsx +98 -0
- package/src/atoms/stagger/stagger.types.ts +35 -0
- package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +3 -3
- package/src/organisms/snackbar/snackbar.tsx +20 -40
- package/src/organisms/snackbar/snackbar.types.ts +6 -8
- package/lib/module/atoms/box/a-box.js +0 -6
- package/lib/module/atoms/box/a-box.js.map +0 -1
- package/lib/module/motion.js +0 -19
- package/lib/module/motion.js.map +0 -1
- package/lib/typescript/atoms/box/a-box.d.ts +0 -49
- package/lib/typescript/motion.d.ts +0 -8
- package/src/atoms/box/a-box.ts +0 -5
- package/src/motion.ts +0 -13
|
@@ -1,36 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 {
|
|
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
|
-
<
|
|
31
|
+
<Box
|
|
32
32
|
aria-hidden
|
|
33
33
|
bg="fill.quaternary"
|
|
34
34
|
borderRadius={borderRadius}
|
|
@@ -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 {
|
|
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
|
-
<
|
|
27
|
+
<Box
|
|
28
28
|
bg="surface.secondary"
|
|
29
29
|
bottom={0}
|
|
30
30
|
left={0}
|
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
*
|
|
47
|
-
*
|
|
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:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
//
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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 {
|
|
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
|
-
/**
|
|
37
|
-
entering?:
|
|
38
|
-
/**
|
|
39
|
-
exiting?:
|
|
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 +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":[]}
|
package/lib/module/motion.js
DELETED
|
@@ -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
|
package/lib/module/motion.js.map
DELETED
|
@@ -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;
|
package/src/atoms/box/a-box.ts
DELETED
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
|
-
}
|