@hzblj/kara-components 0.11.0 → 0.13.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 +47 -0
- package/README.md +2 -1
- 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/icon/icon.android.js +1 -1
- package/lib/module/atoms/icon/icon.android.js.map +1 -1
- package/lib/module/atoms/icon/icon.ios.js +1 -1
- package/lib/module/atoms/icon/icon.ios.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 +63 -16
- package/lib/module/atoms/pressable/pressable.js.map +1 -1
- package/lib/module/atoms/rn-icon/index.js +1 -1
- package/lib/module/atoms/rn-icon/index.js.map +1 -1
- package/lib/module/atoms/rn-icon/rn-icon.js +26 -0
- package/lib/module/atoms/rn-icon/rn-icon.js.map +1 -0
- 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/segmented-control/segmented-control.android.js +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.android.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/slider/slider.android.js +1 -1
- package/lib/module/atoms/slider/slider.android.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/atoms/switch/switch.android.js +1 -1
- package/lib/module/atoms/switch/switch.android.js.map +1 -1
- package/lib/module/glyph/rn-glyph.android.js +8 -3
- package/lib/module/glyph/rn-glyph.android.js.map +1 -1
- package/lib/module/index.js +0 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.android.js +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.android.js.map +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.ios.js +1 -1
- package/lib/module/molecules/navigation-bar/navigation-bar.ios.js.map +1 -1
- 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/context-menu/context-menu.android.js +1 -1
- package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.ios.js +1 -1
- package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.ios.js +1 -1
- package/lib/module/organisms/fab-menu/fab-menu.ios.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/module/organisms/swipe-actions/swipe-actions.ios.js +1 -1
- package/lib/module/organisms/swipe-actions/swipe-actions.ios.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/icon/icon.types.d.ts +6 -15
- package/lib/typescript/atoms/index.d.ts +1 -0
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +5 -4
- package/lib/typescript/atoms/rn-icon/rn-icon.d.ts +6 -1
- 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/index.d.ts +0 -1
- package/lib/typescript/organisms/snackbar/snackbar.types.d.ts +6 -8
- package/package.json +6 -4
- package/src/atoms/box/box.tsx +3 -0
- package/src/atoms/box/index.ts +0 -1
- package/src/atoms/icon/icon.android.tsx +1 -1
- package/src/atoms/icon/icon.ios.tsx +1 -1
- package/src/atoms/icon/icon.types.ts +6 -15
- package/src/atoms/index.ts +1 -0
- package/src/atoms/pressable/pressable.tsx +163 -107
- package/src/atoms/pressable/pressable.types.ts +5 -4
- package/src/atoms/rn-icon/rn-icon.tsx +28 -0
- package/src/atoms/rn-text/rn-text.tsx +101 -32
- package/src/atoms/rn-text/rn-text.types.ts +7 -3
- package/src/atoms/segmented-control/segmented-control.android.tsx +1 -1
- package/src/atoms/skeleton/skeleton.tsx +3 -3
- package/src/atoms/slider/slider.android.tsx +1 -1
- 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/atoms/switch/switch.android.tsx +1 -1
- package/src/glyph/rn-glyph.android.tsx +14 -3
- package/src/index.ts +0 -1
- package/src/molecules/navigation-bar/navigation-bar.android.tsx +1 -1
- package/src/molecules/navigation-bar/navigation-bar.ios.tsx +1 -1
- package/src/molecules/scroll-edge-container/scroll-edge-container.android.tsx +3 -3
- package/src/organisms/context-menu/context-menu.android.tsx +1 -1
- package/src/organisms/context-menu/context-menu.ios.tsx +1 -1
- package/src/organisms/fab-menu/fab-menu.ios.tsx +1 -1
- package/src/organisms/snackbar/snackbar.tsx +20 -40
- package/src/organisms/snackbar/snackbar.types.ts +6 -8
- package/src/organisms/swipe-actions/swipe-actions.ios.tsx +1 -1
- package/lib/module/atoms/box/a-box.js +0 -6
- package/lib/module/atoms/box/a-box.js.map +0 -1
- package/lib/module/atoms/rn-icon/rn-icon.android.js +0 -29
- package/lib/module/atoms/rn-icon/rn-icon.android.js.map +0 -1
- package/lib/module/atoms/rn-icon/rn-icon.ios.js +0 -29
- package/lib/module/atoms/rn-icon/rn-icon.ios.js.map +0 -1
- package/lib/module/haptics/haptics.js +0 -56
- package/lib/module/haptics/haptics.js.map +0 -1
- package/lib/module/haptics/index.js +0 -5
- package/lib/module/haptics/index.js.map +0 -1
- package/lib/module/haptics/use-haptic-feedback.js +0 -33
- package/lib/module/haptics/use-haptic-feedback.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/atoms/rn-icon/rn-icon.ios.d.ts +0 -3
- package/lib/typescript/haptics/haptics.d.ts +0 -61
- package/lib/typescript/haptics/index.d.ts +0 -2
- package/lib/typescript/haptics/use-haptic-feedback.d.ts +0 -15
- package/lib/typescript/motion.d.ts +0 -8
- package/src/atoms/box/a-box.ts +0 -5
- package/src/atoms/rn-icon/rn-icon.android.tsx +0 -28
- package/src/atoms/rn-icon/rn-icon.ios.tsx +0 -28
- package/src/haptics/haptics.ts +0 -68
- package/src/haptics/index.ts +0 -2
- package/src/haptics/use-haptic-feedback.ts +0 -36
- package/src/motion.ts +0 -13
|
@@ -1,20 +1,12 @@
|
|
|
1
|
+
import type { NativeIconName } from '@hzblj/expo-native-icons';
|
|
1
2
|
import type { ColorToken, SpaceSize } from '@hzblj/kara-theme';
|
|
2
3
|
import type { ReactNode } from 'react';
|
|
3
|
-
declare global {
|
|
4
|
-
/**
|
|
5
|
-
* The app's icon names, one key per SVG in its icon folder. Written by the config plugin
|
|
6
|
-
* (`plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]`) into `kara-icons.d.ts`, or by
|
|
7
|
-
* `npx kara-icons`. Empty until then. Global, so the declaration reaches it without resolving any kara package.
|
|
8
|
-
*/
|
|
9
|
-
interface KaraIcons {
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
type DeclaredIconName = keyof KaraIcons & string;
|
|
13
4
|
/**
|
|
14
|
-
* One of the app's icons
|
|
15
|
-
*
|
|
5
|
+
* One of the app's icons: a key of the global `NativeIcons`, which the config plugin writes into `native-icons.d.ts`
|
|
6
|
+
* (`plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]`) or `npx kara-icons` does. Once it exists only
|
|
7
|
+
* those names type-check, so an icon removed from the folder fails at every call site; before that, any string.
|
|
16
8
|
*/
|
|
17
|
-
export type IconName =
|
|
9
|
+
export type IconName = NativeIconName;
|
|
18
10
|
/**
|
|
19
11
|
* One of the app's icons, in a host: SwiftUI `Image` from the asset catalog on iOS, Material's `Icon` with the vector
|
|
20
12
|
* drawable on Android. `RnIcon` draws the same asset outside one.
|
|
@@ -24,7 +16,7 @@ export type IconProps = {
|
|
|
24
16
|
accessibilityLabel?: string;
|
|
25
17
|
/** The icon's tint. `content.primary` by default. */
|
|
26
18
|
color?: ColorToken;
|
|
27
|
-
/** Which of the app's icons
|
|
19
|
+
/** Which of the app's icons: a key of `NativeIcons`, one per SVG in the icon folder. */
|
|
28
20
|
name: IconName;
|
|
29
21
|
/** Its width and height, a spacing token. `s24` by default. */
|
|
30
22
|
size?: SpaceSize;
|
|
@@ -32,4 +24,3 @@ export type IconProps = {
|
|
|
32
24
|
testID?: string;
|
|
33
25
|
};
|
|
34
26
|
export type IconComponent = (props: IconProps) => ReactNode;
|
|
35
|
-
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Haptic } from '@hzblj/expo-rich-haptics';
|
|
2
|
+
import type { AccessibilityProps, MotionProps, ResponsiveViewSx } from '@hzblj/kara-sx';
|
|
2
3
|
import type { OpacitySize } from '@hzblj/kara-theme';
|
|
3
4
|
import type { ReactNode } from 'react';
|
|
4
|
-
import type { Haptic } from '../../haptics';
|
|
5
5
|
/** How the content answers a press: it eases down, it dims, or it stays still. */
|
|
6
6
|
export type PressEffect = 'none' | 'opacity' | 'scale';
|
|
7
7
|
/**
|
|
@@ -9,9 +9,10 @@ export type PressEffect = 'none' | 'opacity' | 'scale';
|
|
|
9
9
|
* presses its own way by default: iOS eases the content down to 96%, Android draws Material's ripple over it,
|
|
10
10
|
* clipped to the pressable's shape. `pressEffect` picks the same answer on both, and Android keeps its ripple on top
|
|
11
11
|
* unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`). Screen readers read
|
|
12
|
-
* it as a button unless `accessibilityRole` names another role.
|
|
12
|
+
* it as a button unless `accessibilityRole` names another role. It takes the motion props a `Box` does: `animate` a
|
|
13
|
+
* selected state's `bg`, or let it arrive with `entering`. The React Native tier: it needs no host.
|
|
13
14
|
*/
|
|
14
|
-
export type PressableProps = ResponsiveViewSx & AccessibilityProps & {
|
|
15
|
+
export type PressableProps = ResponsiveViewSx & Omit<MotionProps, 'animated'> & AccessibilityProps & {
|
|
15
16
|
/** The pressable content. */
|
|
16
17
|
children?: ReactNode;
|
|
17
18
|
/** Ignores presses: no `onPress`, no haptic, no press effect or ripple. */
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RnIconComponent } from './rn-icon.types';
|
|
2
|
+
/**
|
|
3
|
+
* `@hzblj/expo-native-icons`' `NativeIcon`: a `UIImageView` over the app's asset-catalog icon on iOS, one view drawing
|
|
4
|
+
* its vector drawable on Android. What a list row wants, rather than a SwiftUI or Compose host per row.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RnIcon: RnIconComponent;
|
|
@@ -1,17 +1,20 @@
|
|
|
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
|
* React Native's `Text` in a theme typography variant: the React Native tier, for text outside a native host. It takes
|
|
7
8
|
* `Box`'s `sx` surface for its own box (margins, flex, padding, a background), all but `align`, which here aligns the
|
|
8
|
-
* lines.
|
|
9
|
+
* lines, and the motion props a `Box` takes: `entering="fadeUp"` makes it an `Animated.Text` by itself.
|
|
9
10
|
*/
|
|
10
|
-
export type RnTextProps = Omit<RNTextProps, 'style'> & Omit<ResponsiveViewSx, 'align'> & {
|
|
11
|
+
export type RnTextProps = Omit<RNTextProps, 'style'> & MotionProps & Omit<ResponsiveViewSx, 'align'> & {
|
|
11
12
|
/** Horizontal alignment, as React Native's `textAlign`. */
|
|
12
13
|
align?: 'center' | 'left' | 'right';
|
|
13
14
|
/** The text's colour. `content.primary` by default. */
|
|
14
15
|
color?: ColorToken;
|
|
16
|
+
/** Goes last, over the variant and the `sx`: an animated style from `useAnimatedStyle`, which it renders through Reanimated for. */
|
|
17
|
+
style?: StyleProp<AnimatedStyle<TextStyle>>;
|
|
15
18
|
/** The typography variant. `body` by default. */
|
|
16
19
|
variant?: TextVariant;
|
|
17
20
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** A preset by name, or one adjusted: `{preset: 'fadeUp', duration: 'slow'}`. Its `delay` is where the first one starts. */
|
|
5
|
+
export type StaggerMotion = MotionPreset | MotionPresetConfig;
|
|
6
|
+
/**
|
|
7
|
+
* A `Box` whose children arrive one after another: each takes `entering` a step later than the one before. Children
|
|
8
|
+
* that arrive together are staggered among themselves, the first ones as much as a later batch (the next page of a
|
|
9
|
+
* list), while a single child added later arrives at once. A `Box`, `RnText` or `Pressable` child takes the motion
|
|
10
|
+
* itself; any other child is wrapped in a `Box` that does. With motion reduced every child fades, still one after
|
|
11
|
+
* another. The React Native tier: it needs no host.
|
|
12
|
+
*/
|
|
13
|
+
export type StaggerProps = ResponsiveViewSx & {
|
|
14
|
+
/** What arrives. A child with an `entering` preset of its own keeps it and only takes the delay. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** How each child arrives. `fadeUp` by default. */
|
|
17
|
+
entering?: StaggerMotion;
|
|
18
|
+
/** How a child leaves when it is removed. They leave together, not one after another; none by default. */
|
|
19
|
+
exiting?: StaggerMotion;
|
|
20
|
+
/** Which end arrives first. `start` by default. */
|
|
21
|
+
from?: 'end' | 'start';
|
|
22
|
+
/** The step between one child and the next, a duration token. `stagger` (40 ms) by default, `micro` for a slower one. */
|
|
23
|
+
interval?: DurationSize;
|
|
24
|
+
/**
|
|
25
|
+
* Moves the other children into place when one arrives or leaves, and the stagger itself to its new size: `true` on
|
|
26
|
+
* the `smooth` spring, or a spring token. Off by default. Give what sits after it `layout` too, so it follows.
|
|
27
|
+
*/
|
|
28
|
+
layout?: boolean | SpringName;
|
|
29
|
+
/** Test identifier, forwarded to the view. */
|
|
30
|
+
testID?: string;
|
|
31
|
+
};
|
|
32
|
+
export type StaggerComponent = (props: StaggerProps) => ReactNode;
|
|
@@ -1,7 +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
|
-
type LayoutAnimation = ComponentProps<typeof Animated.View>['entering'];
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
5
4
|
/** How it comes and goes. */
|
|
6
5
|
export type SnackbarAnimation = 'fade' | 'scale' | 'slide';
|
|
7
6
|
/** How long it stays: Material's short (4 s) and long (10 s), never, or a number of milliseconds. */
|
|
@@ -28,10 +27,10 @@ export type SnackbarProps = {
|
|
|
28
27
|
dismissible?: boolean;
|
|
29
28
|
/** How long it stays up before `onDismiss` is called. `indefinite` by default: it never hides itself. */
|
|
30
29
|
duration?: SnackbarDuration;
|
|
31
|
-
/**
|
|
32
|
-
entering?:
|
|
33
|
-
/**
|
|
34
|
-
exiting?:
|
|
30
|
+
/** How it arrives instead: a motion preset (`'scale'`), or Reanimated's own, e.g. `FadeInDown.springify()`. It wins over `animation`. */
|
|
31
|
+
entering?: SxEntryExit;
|
|
32
|
+
/** How it leaves instead: a motion preset, or Reanimated's own. It wins over `animation`. */
|
|
33
|
+
exiting?: SxEntryExit;
|
|
35
34
|
/** The message. */
|
|
36
35
|
message: string;
|
|
37
36
|
/** Between the snackbar and its edge, a space token. `s24` by default; raise it above a tab bar. */
|
|
@@ -46,4 +45,3 @@ export type SnackbarProps = {
|
|
|
46
45
|
visible: boolean;
|
|
47
46
|
};
|
|
48
47
|
export type SnackbarComponent = (props: SnackbarProps) => ReactNode;
|
|
49
|
-
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
|
|
5
5
|
"author": "Jan Blazej",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,9 +59,11 @@
|
|
|
59
59
|
]
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@hzblj/
|
|
63
|
-
"@hzblj/
|
|
64
|
-
"@hzblj/kara-
|
|
62
|
+
"@hzblj/expo-native-icons": "^0.1.0",
|
|
63
|
+
"@hzblj/expo-rich-haptics": "^0.1.0",
|
|
64
|
+
"@hzblj/kara-native": "^0.13.0",
|
|
65
|
+
"@hzblj/kara-sx": "^0.13.0",
|
|
66
|
+
"@hzblj/kara-theme": "^0.13.0",
|
|
65
67
|
"sf-symbols-typescript": "^2.2.0"
|
|
66
68
|
},
|
|
67
69
|
"peerDependencies": {
|
package/src/atoms/box/box.tsx
CHANGED
|
@@ -4,5 +4,8 @@ import {View} from 'react-native'
|
|
|
4
4
|
/**
|
|
5
5
|
* A React Native `View` that takes the design system's `sx` props. The `sx()` HOC is the whole
|
|
6
6
|
* implementation — Box is just its first caller, and any other style-taking component gets the same.
|
|
7
|
+
*
|
|
8
|
+
* That includes motion: `entering`, `exiting`, `layout`, `animate` or an animated `style` make it an `Animated.View`
|
|
9
|
+
* from its first frame, and without them it stays a plain `View`.
|
|
7
10
|
*/
|
|
8
11
|
export const Box = sx(View)
|
package/src/atoms/box/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Image} from '@expo/ui/swift-ui'
|
|
2
2
|
import {accessibilityHidden, frame, accessibilityLabel as labelModifier, resizable} from '@expo/ui/swift-ui/modifiers'
|
|
3
|
-
import {iconResourceName} from '@hzblj/
|
|
3
|
+
import {iconResourceName} from '@hzblj/expo-native-icons'
|
|
4
4
|
import {spacing, useAppTheme} from '@hzblj/kara-theme'
|
|
5
5
|
import {useMemo} from 'react'
|
|
6
6
|
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
+
import type {NativeIconName} from '@hzblj/expo-native-icons'
|
|
1
2
|
import type {ColorToken, SpaceSize} from '@hzblj/kara-theme'
|
|
2
3
|
import type {ReactNode} from 'react'
|
|
3
4
|
|
|
4
|
-
declare global {
|
|
5
|
-
/**
|
|
6
|
-
* The app's icon names, one key per SVG in its icon folder. Written by the config plugin
|
|
7
|
-
* (`plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]`) into `kara-icons.d.ts`, or by
|
|
8
|
-
* `npx kara-icons`. Empty until then. Global, so the declaration reaches it without resolving any kara package.
|
|
9
|
-
*/
|
|
10
|
-
interface KaraIcons {}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type DeclaredIconName = keyof KaraIcons & string
|
|
14
|
-
|
|
15
5
|
/**
|
|
16
|
-
* One of the app's icons
|
|
17
|
-
*
|
|
6
|
+
* One of the app's icons: a key of the global `NativeIcons`, which the config plugin writes into `native-icons.d.ts`
|
|
7
|
+
* (`plugins: [['@hzblj/kara-ui', {icons: {source: './assets/icons'}}]]`) or `npx kara-icons` does. Once it exists only
|
|
8
|
+
* those names type-check, so an icon removed from the folder fails at every call site; before that, any string.
|
|
18
9
|
*/
|
|
19
|
-
export type IconName =
|
|
10
|
+
export type IconName = NativeIconName
|
|
20
11
|
|
|
21
12
|
/**
|
|
22
13
|
* One of the app's icons, in a host: SwiftUI `Image` from the asset catalog on iOS, Material's `Icon` with the vector
|
|
@@ -27,7 +18,7 @@ export type IconProps = {
|
|
|
27
18
|
accessibilityLabel?: string
|
|
28
19
|
/** The icon's tint. `content.primary` by default. */
|
|
29
20
|
color?: ColorToken
|
|
30
|
-
/** Which of the app's icons
|
|
21
|
+
/** Which of the app's icons: a key of `NativeIcons`, one per SVG in the icon folder. */
|
|
31
22
|
name: IconName
|
|
32
23
|
/** Its width and height, a spacing token. `s24` by default. */
|
|
33
24
|
size?: SpaceSize
|
package/src/atoms/index.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {haptics} from '@hzblj/expo-rich-haptics'
|
|
2
|
+
import {
|
|
3
|
+
type AnimatableKey,
|
|
4
|
+
accessibilityProps,
|
|
5
|
+
markMotionComponent,
|
|
6
|
+
type SxAnimate,
|
|
7
|
+
splitAccessibility,
|
|
8
|
+
spring,
|
|
9
|
+
timing,
|
|
10
|
+
useSplitSxMotion,
|
|
11
|
+
} from '@hzblj/kara-sx'
|
|
12
|
+
import {opacity, type SpringName, springs, useAppTheme, useReducedMotion, withOpacity} from '@hzblj/kara-theme'
|
|
3
13
|
import {useCallback, useMemo} from 'react'
|
|
4
14
|
import {Platform, Pressable as RNPressable, type ViewStyle} from 'react-native'
|
|
5
|
-
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'
|
|
15
|
+
import Animated, {useAnimatedStyle, useSharedValue, withSpring, withTiming} from 'react-native-reanimated'
|
|
6
16
|
|
|
7
|
-
import {haptics} from '../../haptics'
|
|
8
|
-
import {timing} from '../../motion'
|
|
9
17
|
import {splitForRipple} from '../../ripple-clip'
|
|
10
18
|
import type {PressableComponent, PressableProps} from './pressable.types'
|
|
11
19
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The scale follows the finger on springs, as UIKit's own presses do: down on `interactive`, back on `smooth`, neither
|
|
22
|
+
* with a bounce (a press is not a toy). A spring keeps the speed it has, so a tap let go halfway down eases back instead
|
|
23
|
+
* of turning on the spot, which a timed curve restarted from rest cannot do.
|
|
24
|
+
*/
|
|
25
|
+
const SCALE_IN = spring('interactive')
|
|
26
|
+
const SCALE_OUT = spring('smooth')
|
|
15
27
|
|
|
16
28
|
/** A dim comes in at once and leaves a little slower, as UIKit's own highlight does. */
|
|
17
29
|
const DIM_IN = timing('micro', 'out')
|
|
@@ -21,111 +33,155 @@ const PLATFORM_EFFECT = Platform.OS === 'ios' ? 'scale' : 'none'
|
|
|
21
33
|
|
|
22
34
|
const AnimatedPressable = Animated.createAnimatedComponent(RNPressable)
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
disabled = false,
|
|
27
|
-
haptic = false,
|
|
28
|
-
onPress,
|
|
29
|
-
pressEffect = PLATFORM_EFFECT,
|
|
30
|
-
pressedOpacity = 'o60',
|
|
31
|
-
pressedScale = 0.96,
|
|
32
|
-
ripple: withRipple = true,
|
|
33
|
-
testID,
|
|
34
|
-
...props
|
|
35
|
-
}: PressableProps) => {
|
|
36
|
-
const [accessibility, ownProps] = splitAccessibility(props)
|
|
37
|
-
const [resolved, rest] = useSplitSx(ownProps)
|
|
38
|
-
const reduced = useReducedMotion()
|
|
39
|
-
// With motion reduced the press still shows, and nothing moves: a scale dims instead.
|
|
40
|
-
const effect = reduced && pressEffect === 'scale' ? 'opacity' : pressEffect
|
|
41
|
-
// 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
|
|
42
|
-
const pressed = useSharedValue(0)
|
|
43
|
-
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
44
|
-
const a11y = accessibilityProps(accessibility, 'button')
|
|
45
|
-
const dimmed = opacity[pressedOpacity]
|
|
46
|
-
|
|
47
|
-
const theme = useAppTheme()
|
|
48
|
-
const isAndroid = Platform.OS === 'android'
|
|
49
|
-
|
|
50
|
-
const rStyle = useAnimatedStyle(() => {
|
|
51
|
-
if (effect === 'scale') {
|
|
52
|
-
return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
|
|
53
|
-
}
|
|
36
|
+
/** What `animate` covers by default, less the transform. */
|
|
37
|
+
const VISUAL_KEYS: readonly AnimatableKey[] = ['bg', 'borderColor', 'opacity']
|
|
54
38
|
|
|
55
|
-
|
|
56
|
-
}, [dimmed, effect, pressedScale])
|
|
57
|
-
|
|
58
|
-
// Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
|
|
59
|
-
// the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
|
|
60
|
-
const ripple = useMemo(
|
|
61
|
-
() =>
|
|
62
|
-
isAndroid && withRipple
|
|
63
|
-
? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
|
|
64
|
-
: undefined,
|
|
65
|
-
[isAndroid, theme, withRipple]
|
|
66
|
-
)
|
|
67
|
-
const androidStyle = useMemo(
|
|
68
|
-
() => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
|
|
69
|
-
[isAndroid, resolved]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
73
|
-
const onPressIn = useCallback(() => {
|
|
74
|
-
if (effect !== 'none') {
|
|
75
|
-
pressed.value = withTiming(1, effect === 'scale' ? SCALE_IN : DIM_IN)
|
|
76
|
-
}
|
|
39
|
+
const TRANSFORM_KEYS = new Set<AnimatableKey>(['offsetX', 'offsetY', 'rotate', 'scale'])
|
|
77
40
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The press scale owns the transform, so `animate` leaves it alone: a transition on the same transform would take it
|
|
43
|
+
* back from the press at every re-render. A duration or spring token keeps its timing.
|
|
44
|
+
*/
|
|
45
|
+
const withoutTransform = (animate: SxAnimate | undefined): SxAnimate | undefined => {
|
|
46
|
+
if (animate === undefined || animate === false) {
|
|
47
|
+
return animate
|
|
48
|
+
}
|
|
82
49
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
50
|
+
if (animate === true) {
|
|
51
|
+
return {keys: VISUAL_KEYS}
|
|
52
|
+
}
|
|
86
53
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
54
|
+
if (typeof animate === 'string') {
|
|
55
|
+
return animate in springs
|
|
56
|
+
? {keys: VISUAL_KEYS, spring: animate as SpringName}
|
|
57
|
+
: {duration: animate as Exclude<typeof animate, SpringName>, keys: VISUAL_KEYS}
|
|
58
|
+
}
|
|
91
59
|
|
|
92
|
-
|
|
93
|
-
|
|
60
|
+
return {...animate, keys: (animate.keys ?? VISUAL_KEYS).filter(key => !TRANSFORM_KEYS.has(key))}
|
|
61
|
+
}
|
|
94
62
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
63
|
+
export const Pressable: PressableComponent = markMotionComponent(
|
|
64
|
+
({
|
|
65
|
+
children,
|
|
66
|
+
disabled = false,
|
|
67
|
+
haptic = false,
|
|
68
|
+
onPress,
|
|
69
|
+
pressEffect = PLATFORM_EFFECT,
|
|
70
|
+
pressedOpacity = 'o60',
|
|
71
|
+
pressedScale = 0.96,
|
|
72
|
+
ripple: withRipple = true,
|
|
73
|
+
testID,
|
|
74
|
+
...props
|
|
75
|
+
}: PressableProps) => {
|
|
76
|
+
const [accessibility, ownProps] = splitAccessibility(props)
|
|
77
|
+
const reduced = useReducedMotion()
|
|
78
|
+
// With motion reduced the press still shows, and nothing moves: a scale dims instead.
|
|
79
|
+
const effect = reduced && pressEffect === 'scale' ? 'opacity' : pressEffect
|
|
80
|
+
// Always a Reanimated view (the press effect is one), so the motion props need no latch here.
|
|
81
|
+
const [resolved, motion, rest] = useSplitSxMotion(
|
|
82
|
+
effect === 'scale' && ownProps.animate !== undefined
|
|
83
|
+
? {...ownProps, animate: withoutTransform(ownProps.animate as SxAnimate)}
|
|
84
|
+
: ownProps
|
|
85
|
+
)
|
|
86
|
+
// 0 at rest, 1 held: one value drives whichever effect is picked, so a change of effect needs no second animation.
|
|
87
|
+
const pressed = useSharedValue(0)
|
|
88
|
+
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
89
|
+
const a11y = accessibilityProps(accessibility, 'button')
|
|
90
|
+
const dimmed = opacity[pressedOpacity]
|
|
91
|
+
|
|
92
|
+
const theme = useAppTheme()
|
|
93
|
+
const isAndroid = Platform.OS === 'android'
|
|
94
|
+
|
|
95
|
+
const rStyle = useAnimatedStyle(() => {
|
|
96
|
+
if (effect === 'scale') {
|
|
97
|
+
return {transform: [{scale: 1 - (1 - pressedScale) * pressed.value}]}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return effect === 'opacity' ? {opacity: 1 - (1 - dimmed) * pressed.value} : {}
|
|
101
|
+
}, [dimmed, effect, pressedScale])
|
|
102
|
+
|
|
103
|
+
// Android's press is Material's: a ripple over the content (the pressed state layer, `onSurface` at 10 %) clipped to
|
|
104
|
+
// the shape by an outer view (`splitForRipple`). The effect, when there is one, moves that outer view, ripple and all.
|
|
105
|
+
const ripple = useMemo(
|
|
106
|
+
() =>
|
|
107
|
+
isAndroid && withRipple
|
|
108
|
+
? {color: withOpacity(String(theme.color('content.primary')), 'o10'), foreground: true}
|
|
109
|
+
: undefined,
|
|
110
|
+
[isAndroid, theme, withRipple]
|
|
111
|
+
)
|
|
112
|
+
const androidStyle = useMemo(
|
|
113
|
+
() => (isAndroid ? splitForRipple(resolved as ViewStyle) : undefined),
|
|
114
|
+
[isAndroid, resolved]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
118
|
+
const onPressIn = useCallback(() => {
|
|
119
|
+
if (effect === 'scale') {
|
|
120
|
+
pressed.value = withSpring(1, SCALE_IN)
|
|
121
|
+
} else if (effect === 'opacity') {
|
|
122
|
+
pressed.value = withTiming(1, DIM_IN)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (feedback) {
|
|
126
|
+
haptics.prepare(feedback)
|
|
127
|
+
}
|
|
128
|
+
}, [effect, feedback, pressed])
|
|
129
|
+
|
|
130
|
+
const onPressOut = useCallback(() => {
|
|
131
|
+
pressed.value = effect === 'scale' ? withSpring(0, SCALE_OUT) : withTiming(0, DIM_OUT)
|
|
132
|
+
}, [effect, pressed])
|
|
133
|
+
|
|
134
|
+
const onPressWithHaptic = useCallback(() => {
|
|
135
|
+
if (feedback) {
|
|
136
|
+
haptics.play(feedback)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onPress?.()
|
|
140
|
+
}, [feedback, onPress])
|
|
141
|
+
|
|
142
|
+
if (androidStyle) {
|
|
143
|
+
return (
|
|
144
|
+
<Animated.View
|
|
145
|
+
entering={motion.entering}
|
|
146
|
+
exiting={motion.exiting}
|
|
147
|
+
layout={motion.layout}
|
|
148
|
+
style={[androidStyle.outer, motion.style, rStyle]}
|
|
109
149
|
>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
<RNPressable
|
|
151
|
+
{...a11y}
|
|
152
|
+
android_ripple={ripple}
|
|
153
|
+
disabled={disabled}
|
|
154
|
+
hitSlop={8}
|
|
155
|
+
onPress={onPressWithHaptic}
|
|
156
|
+
onPressIn={onPressIn}
|
|
157
|
+
onPressOut={onPressOut}
|
|
158
|
+
style={androidStyle.inner}
|
|
159
|
+
testID={testID}
|
|
160
|
+
{...rest}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</RNPressable>
|
|
164
|
+
</Animated.View>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<AnimatedPressable
|
|
170
|
+
{...a11y}
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
entering={motion.entering}
|
|
173
|
+
exiting={motion.exiting}
|
|
174
|
+
hitSlop={8}
|
|
175
|
+
layout={motion.layout}
|
|
176
|
+
onPress={onPressWithHaptic}
|
|
177
|
+
onPressIn={onPressIn}
|
|
178
|
+
onPressOut={onPressOut}
|
|
179
|
+
style={[resolved, motion.style, rStyle]}
|
|
180
|
+
testID={testID}
|
|
181
|
+
{...rest}
|
|
182
|
+
>
|
|
183
|
+
{children}
|
|
184
|
+
</AnimatedPressable>
|
|
113
185
|
)
|
|
114
186
|
}
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<AnimatedPressable
|
|
118
|
-
{...a11y}
|
|
119
|
-
disabled={disabled}
|
|
120
|
-
hitSlop={8}
|
|
121
|
-
onPress={onPressWithHaptic}
|
|
122
|
-
onPressIn={onPressIn}
|
|
123
|
-
onPressOut={onPressOut}
|
|
124
|
-
style={[resolved, rStyle]}
|
|
125
|
-
testID={testID}
|
|
126
|
-
{...rest}
|
|
127
|
-
>
|
|
128
|
-
{children}
|
|
129
|
-
</AnimatedPressable>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
187
|
+
)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {Haptic} from '@hzblj/expo-rich-haptics'
|
|
2
|
+
import type {AccessibilityProps, MotionProps, ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
2
3
|
import type {OpacitySize} from '@hzblj/kara-theme'
|
|
3
4
|
import type {ReactNode} from 'react'
|
|
4
5
|
|
|
5
|
-
import type {Haptic} from '../../haptics'
|
|
6
|
-
|
|
7
6
|
/** How the content answers a press: it eases down, it dims, or it stays still. */
|
|
8
7
|
export type PressEffect = 'none' | 'opacity' | 'scale'
|
|
9
8
|
|
|
@@ -12,9 +11,11 @@ export type PressEffect = 'none' | 'opacity' | 'scale'
|
|
|
12
11
|
* presses its own way by default: iOS eases the content down to 96%, Android draws Material's ripple over it,
|
|
13
12
|
* clipped to the pressable's shape. `pressEffect` picks the same answer on both, and Android keeps its ripple on top
|
|
14
13
|
* unless `ripple` is off. Give it the `borderRadius` of what it wraps (`full` around a `GlassView`). Screen readers read
|
|
15
|
-
* it as a button unless `accessibilityRole` names another role.
|
|
14
|
+
* it as a button unless `accessibilityRole` names another role. It takes the motion props a `Box` does: `animate` a
|
|
15
|
+
* selected state's `bg`, or let it arrive with `entering`. The React Native tier: it needs no host.
|
|
16
16
|
*/
|
|
17
17
|
export type PressableProps = ResponsiveViewSx &
|
|
18
|
+
Omit<MotionProps, 'animated'> &
|
|
18
19
|
AccessibilityProps & {
|
|
19
20
|
/** The pressable content. */
|
|
20
21
|
children?: ReactNode
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {NativeIcon} from '@hzblj/expo-native-icons'
|
|
2
|
+
import {spacing, useAppTheme} from '@hzblj/kara-theme'
|
|
3
|
+
|
|
4
|
+
import type {RnIconComponent, RnIconProps} from './rn-icon.types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@hzblj/expo-native-icons`' `NativeIcon`: a `UIImageView` over the app's asset-catalog icon on iOS, one view drawing
|
|
8
|
+
* its vector drawable on Android. What a list row wants, rather than a SwiftUI or Compose host per row.
|
|
9
|
+
*/
|
|
10
|
+
export const RnIcon: RnIconComponent = ({
|
|
11
|
+
accessibilityLabel,
|
|
12
|
+
color = 'content.primary',
|
|
13
|
+
name,
|
|
14
|
+
size = 's24',
|
|
15
|
+
testID,
|
|
16
|
+
}: RnIconProps) => {
|
|
17
|
+
const theme = useAppTheme()
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<NativeIcon
|
|
21
|
+
accessibilityLabel={accessibilityLabel}
|
|
22
|
+
color={theme.color(color)}
|
|
23
|
+
name={name}
|
|
24
|
+
size={spacing[size]}
|
|
25
|
+
testID={testID}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
}
|