@hzblj/kara-components 0.4.1 → 0.6.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 +41 -0
- package/lib/module/atoms/chip/chip.js +1 -1
- package/lib/module/atoms/chip/chip.js.map +1 -1
- package/lib/module/atoms/pressable/pressable.js +11 -5
- package/lib/module/atoms/pressable/pressable.js.map +1 -1
- package/lib/module/atoms/segmented-control/segmented-control.js +14 -2
- package/lib/module/atoms/segmented-control/segmented-control.js.map +1 -1
- package/lib/module/atoms/slider/slider.android.js +26 -2
- package/lib/module/atoms/slider/slider.android.js.map +1 -1
- package/lib/module/atoms/switch/switch.android.js +11 -4
- package/lib/module/atoms/switch/switch.android.js.map +1 -1
- package/lib/module/haptics/haptics.js +56 -0
- package/lib/module/haptics/haptics.js.map +1 -0
- package/lib/module/haptics/index.js +5 -0
- package/lib/module/haptics/index.js.map +1 -0
- package/lib/module/haptics/use-haptic-feedback.js +33 -0
- package/lib/module/haptics/use-haptic-feedback.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.android.js +4 -3
- package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
- package/lib/module/organisms/context-menu/context-menu.ios.js +2 -0
- package/lib/module/organisms/context-menu/context-menu.ios.js.map +1 -1
- package/lib/typescript/atoms/pressable/pressable.types.d.ts +3 -1
- package/lib/typescript/atoms/segmented-control/segmented-control.d.ts +4 -0
- package/lib/typescript/haptics/haptics.d.ts +61 -0
- package/lib/typescript/haptics/index.d.ts +2 -0
- package/lib/typescript/haptics/use-haptic-feedback.d.ts +15 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/organisms/context-menu/context-menu.types.d.ts +8 -0
- package/package.json +4 -6
- package/src/atoms/chip/chip.tsx +1 -1
- package/src/atoms/pressable/pressable.tsx +12 -5
- package/src/atoms/pressable/pressable.types.ts +4 -1
- package/src/atoms/segmented-control/segmented-control.tsx +15 -2
- package/src/atoms/slider/slider.android.tsx +33 -2
- package/src/atoms/switch/switch.android.tsx +15 -5
- package/src/haptics/haptics.ts +68 -0
- package/src/haptics/index.ts +2 -0
- package/src/haptics/use-haptic-feedback.ts +36 -0
- package/src/index.ts +1 -0
- package/src/organisms/context-menu/context-menu.android.tsx +4 -4
- package/src/organisms/context-menu/context-menu.ios.tsx +10 -1
- package/src/organisms/context-menu/context-menu.types.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @hzblj/kara-components
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10](https://github.com/hzblj/kara-ui/pull/10) [`62b7131`](https://github.com/hzblj/kara-ui/commit/62b713143bd03872ecdd89ca8de42d98fe589617) Thanks [@hzblj](https://github.com/hzblj)! - Haptics: kara's own, native on both platforms, named for what happened.
|
|
8
|
+
|
|
9
|
+
- `haptics.success()`, `haptics.selection()`, … — one function per haptic — and `haptics.play(name)` for a name that arrives as data. `selection`, `tick`, `toggleOn`/`toggleOff`, `success`/`warning`/`error`, `longPress`, `dragStart`, `thresholdActivate`/`thresholdDeactivate`, `keyPress`, and the impacts `soft`, `light`, `medium`, `rigid`, `heavy` (`intensity` scales them on iOS). Synchronous, nothing to await.
|
|
10
|
+
- iOS: UIKit's feedback generators, kept alive and warmed up with `haptics.prepare` — `Pressable` prepares its haptic on press-in. Android: the system's own haptic feedback (`View.performHapticFeedback`), with Android 14's constants falling back to older ones — never a raw vibration. Both follow the user's system setting.
|
|
11
|
+
- `useHapticFeedback(trigger, haptic)` plays on every change of a value, never on mount; a function of the previous and next value picks a haptic per change.
|
|
12
|
+
- `haptics.setEnabled(false)` silences every kara haptic, the components' own included.
|
|
13
|
+
- `Pressable`'s `haptic` takes a name as well as `true` (a `light` impact); `Chip` plays `selection`.
|
|
14
|
+
- On Android, where Compose's Material controls play nothing: `Switch` plays `toggleOn`/`toggleOff`, a stepped `Slider` ticks at every stop, `SegmentedControl` plays `selection`. iOS keeps its native controls' own feedback.
|
|
15
|
+
- `Slider`: `step` is the increment on Android too — it was passed to Compose as the number of stops.
|
|
16
|
+
- `expo-haptics` is no longer a peer dependency.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`62b7131`](https://github.com/hzblj/kara-ui/commit/62b713143bd03872ecdd89ca8de42d98fe589617)]:
|
|
21
|
+
- @hzblj/kara-native@0.6.0
|
|
22
|
+
- @hzblj/kara-theme@0.6.0
|
|
23
|
+
- @hzblj/kara-sx@0.6.0
|
|
24
|
+
|
|
25
|
+
## 0.5.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- [#8](https://github.com/hzblj/kara-ui/pull/8) [`3b0a6fb`](https://github.com/hzblj/kara-ui/commit/3b0a6fbc49559720023fece2b5c6189ad4d88bc8) Thanks [@hzblj](https://github.com/hzblj)! - `ZoomTransition`: a push that grows out of the photo it was opened from and shrinks back into it.
|
|
30
|
+
|
|
31
|
+
- iOS 18: UIKit's zoom transition, with the interactive dismissal (swipe down, edge swipe, pinch) gated on the scroll views under the finger. Below iOS 18 the push stays a slide. Android: Material's container transform, both ways.
|
|
32
|
+
- Three pieces from `@hzblj/kara-ui/router`, tied by one identifier: `ZoomTransition.Source` on the list, `ZoomTransition.Enabler` and `ZoomTransition.Target` on the pushed screen; `useZoomTransitionSourceId`, `ZOOM_TRANSITION_PARAM`, `useZoomTransitionParam` and `withZoomTransition` (an Android fade under the transform) around them.
|
|
33
|
+
- Settings both platforms honour sit at the top (`scrim`); the rest is typed apart under `ios` (`dismissGesture`, `blur`, the target's `fit`) and `android` (`duration`, `easing`, `fade`, `path`, corner radii, the target's `fit` and `flex`).
|
|
34
|
+
- `ContextMenu`: with a `ZoomTransition.Source` in the trigger, a tap on the iOS preview zooms the preview's photo straight into the pushed screen. `previewTransition` (`zoom`, `pop`, `dismiss`) picks how the preview leaves.
|
|
35
|
+
- `DurationSize`, the keys of `duration`.
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [[`3b0a6fb`](https://github.com/hzblj/kara-ui/commit/3b0a6fbc49559720023fece2b5c6189ad4d88bc8)]:
|
|
40
|
+
- @hzblj/kara-native@0.5.0
|
|
41
|
+
- @hzblj/kara-theme@0.5.0
|
|
42
|
+
- @hzblj/kara-sx@0.5.0
|
|
43
|
+
|
|
3
44
|
## 0.4.1
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Pressable","RnText","jsx","_jsx","Chip","label","onPress","selected","testID","align","bg","borderColor","borderRadius","borderWidth","haptic","justify","px","py","children","color","variant"],"sourceRoot":"../../../../src","sources":["atoms/chip/chip.tsx"],"mappings":";;AAAA,SAAQA,SAAS,QAAO,uBAAc;AACtC,SAAQC,MAAM,QAAO,qBAAY;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAGjC,OAAO,MAAMC,IAAmB,GAAGA,CAAC;EAACC,KAAK;EAAEC,OAAO;EAAEC,QAAQ,GAAG,KAAK;EAAEC;AAAiB,CAAC,kBACvFL,IAAA,CAACH,SAAS;EACRS,KAAK,EAAC,QAAQ;EACdC,EAAE,EAAEH,QAAQ,GAAG,kBAAkB,GAAG,eAAgB;EACpDI,WAAW,EAAEJ,QAAQ,GAAG,eAAe,GAAG,iBAAkB;EAC5DK,YAAY,EAAC,MAAM;EACnBC,WAAW,EAAE,CAAE;EACfC,MAAM;
|
|
1
|
+
{"version":3,"names":["Pressable","RnText","jsx","_jsx","Chip","label","onPress","selected","testID","align","bg","borderColor","borderRadius","borderWidth","haptic","justify","px","py","children","color","variant"],"sourceRoot":"../../../../src","sources":["atoms/chip/chip.tsx"],"mappings":";;AAAA,SAAQA,SAAS,QAAO,uBAAc;AACtC,SAAQC,MAAM,QAAO,qBAAY;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAGjC,OAAO,MAAMC,IAAmB,GAAGA,CAAC;EAACC,KAAK;EAAEC,OAAO;EAAEC,QAAQ,GAAG,KAAK;EAAEC;AAAiB,CAAC,kBACvFL,IAAA,CAACH,SAAS;EACRS,KAAK,EAAC,QAAQ;EACdC,EAAE,EAAEH,QAAQ,GAAG,kBAAkB,GAAG,eAAgB;EACpDI,WAAW,EAAEJ,QAAQ,GAAG,eAAe,GAAG,iBAAkB;EAC5DK,YAAY,EAAC,MAAM;EACnBC,WAAW,EAAE,CAAE;EACfC,MAAM,EAAC,WAAW;EAClBC,OAAO,EAAC,QAAQ;EAChBT,OAAO,EAAEA,OAAQ;EACjBU,EAAE,EAAC,KAAK;EACRC,EAAE,EAAC,IAAI;EACPT,MAAM,EAAEA,MAAO;EAAAU,QAAA,eAEff,IAAA,CAACF,MAAM;IAACkB,KAAK,EAAEZ,QAAQ,GAAG,gBAAgB,GAAG,mBAAoB;IAACa,OAAO,EAAC,aAAa;IAAAF,QAAA,EACpFb;EAAK,CACA;AAAC,CACA,CACZ","ignoreList":[]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { useSplitSx } from '@hzblj/kara-sx';
|
|
4
|
-
import * as Haptics from 'expo-haptics';
|
|
5
4
|
import { useCallback } from 'react';
|
|
6
5
|
import { Pressable as RNPressable } from 'react-native';
|
|
7
6
|
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
7
|
+
import { haptics } from "../../haptics/index.js";
|
|
8
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
9
|
const SPRING = {
|
|
10
10
|
damping: 20,
|
|
@@ -21,23 +21,29 @@ export const Pressable = ({
|
|
|
21
21
|
}) => {
|
|
22
22
|
const [resolved, rest] = useSplitSx(props);
|
|
23
23
|
const scale = useSharedValue(1);
|
|
24
|
+
const feedback = haptic === true ? 'light' : haptic || undefined;
|
|
24
25
|
const rStyle = useAnimatedStyle(() => ({
|
|
25
26
|
transform: [{
|
|
26
27
|
scale: scale.value
|
|
27
28
|
}]
|
|
28
29
|
}), []);
|
|
30
|
+
|
|
31
|
+
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
29
32
|
const onPressIn = useCallback(() => {
|
|
30
33
|
scale.value = withSpring(0.96, SPRING);
|
|
31
|
-
|
|
34
|
+
if (feedback) {
|
|
35
|
+
haptics.prepare(feedback);
|
|
36
|
+
}
|
|
37
|
+
}, [feedback, scale]);
|
|
32
38
|
const onPressOut = useCallback(() => {
|
|
33
39
|
scale.value = withSpring(1, SPRING);
|
|
34
40
|
}, [scale]);
|
|
35
41
|
const onPressWithHaptic = useCallback(() => {
|
|
36
|
-
if (
|
|
37
|
-
|
|
42
|
+
if (feedback) {
|
|
43
|
+
haptics.play(feedback);
|
|
38
44
|
}
|
|
39
45
|
onPress?.();
|
|
40
|
-
}, [
|
|
46
|
+
}, [feedback, onPress]);
|
|
41
47
|
return /*#__PURE__*/_jsx(AnimatedPressable, {
|
|
42
48
|
disabled: disabled,
|
|
43
49
|
hitSlop: 8,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useSplitSx","
|
|
1
|
+
{"version":3,"names":["useSplitSx","useCallback","Pressable","RNPressable","Animated","useAnimatedStyle","useSharedValue","withSpring","haptics","jsx","_jsx","SPRING","damping","stiffness","AnimatedPressable","createAnimatedComponent","children","disabled","haptic","onPress","testID","props","resolved","rest","scale","feedback","undefined","rStyle","transform","value","onPressIn","prepare","onPressOut","onPressWithHaptic","play","hitSlop","style"],"sourceRoot":"../../../../src","sources":["atoms/pressable/pressable.tsx"],"mappings":";;AAAA,SAAQA,UAAU,QAAO,gBAAgB;AACzC,SAAQC,WAAW,QAAO,OAAO;AACjC,SAAQC,SAAS,IAAIC,WAAW,QAAO,cAAc;AACrD,OAAOC,QAAQ,IAAGC,gBAAgB,EAAEC,cAAc,EAAEC,UAAU,QAAO,yBAAyB;AAE9F,SAAQC,OAAO,QAAO,wBAAe;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAGrC,MAAMC,MAAM,GAAG;EAACC,OAAO,EAAE,EAAE;EAAEC,SAAS,EAAE;AAAG,CAAC;AAE5C,MAAMC,iBAAiB,GAAGV,QAAQ,CAACW,uBAAuB,CAACZ,WAAW,CAAC;AAEvE,OAAO,MAAMD,SAA6B,GAAGA,CAAC;EAC5Cc,QAAQ;EACRC,QAAQ,GAAG,KAAK;EAChBC,MAAM,GAAG,KAAK;EACdC,OAAO;EACPC,MAAM;EACN,GAAGC;AACW,CAAC,KAAK;EACpB,MAAM,CAACC,QAAQ,EAAEC,IAAI,CAAC,GAAGvB,UAAU,CAACqB,KAAK,CAAC;EAC1C,MAAMG,KAAK,GAAGlB,cAAc,CAAC,CAAC,CAAC;EAC/B,MAAMmB,QAAQ,GAAGP,MAAM,KAAK,IAAI,GAAG,OAAO,GAAGA,MAAM,IAAIQ,SAAS;EAEhE,MAAMC,MAAM,GAAGtB,gBAAgB,CAAC,OAAO;IAACuB,SAAS,EAAE,CAAC;MAACJ,KAAK,EAAEA,KAAK,CAACK;IAAK,CAAC;EAAC,CAAC,CAAC,EAAE,EAAE,CAAC;;EAEhF;EACA,MAAMC,SAAS,GAAG7B,WAAW,CAAC,MAAM;IAClCuB,KAAK,CAACK,KAAK,GAAGtB,UAAU,CAAC,IAAI,EAAEI,MAAM,CAAC;IAEtC,IAAIc,QAAQ,EAAE;MACZjB,OAAO,CAACuB,OAAO,CAACN,QAAQ,CAAC;IAC3B;EACF,CAAC,EAAE,CAACA,QAAQ,EAAED,KAAK,CAAC,CAAC;EAErB,MAAMQ,UAAU,GAAG/B,WAAW,CAAC,MAAM;IACnCuB,KAAK,CAACK,KAAK,GAAGtB,UAAU,CAAC,CAAC,EAAEI,MAAM,CAAC;EACrC,CAAC,EAAE,CAACa,KAAK,CAAC,CAAC;EAEX,MAAMS,iBAAiB,GAAGhC,WAAW,CAAC,MAAM;IAC1C,IAAIwB,QAAQ,EAAE;MACZjB,OAAO,CAAC0B,IAAI,CAACT,QAAQ,CAAC;IACxB;IAEAN,OAAO,GAAG,CAAC;EACb,CAAC,EAAE,CAACM,QAAQ,EAAEN,OAAO,CAAC,CAAC;EAEvB,oBACET,IAAA,CAACI,iBAAiB;IAChBG,QAAQ,EAAEA,QAAS;IACnBkB,OAAO,EAAE,CAAE;IACXhB,OAAO,EAAEc,iBAAkB;IAC3BH,SAAS,EAAEA,SAAU;IACrBE,UAAU,EAAEA,UAAW;IACvBI,KAAK,EAAE,CAACd,QAAQ,EAAEK,MAAM,CAAE;IAC1BP,MAAM,EAAEA,MAAO;IAAA,GACXG,IAAI;IAAAP,QAAA,EAEPA;EAAQ,CACQ,CAAC;AAExB,CAAC","ignoreList":[]}
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
import { SegmentedControl as NativeSegmentedControl } from '@expo/ui/community/segmented-control';
|
|
4
4
|
import { useAppTheme } from '@hzblj/kara-theme';
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
|
+
import { Platform } from 'react-native';
|
|
7
|
+
import { haptics } from "../../haptics/index.js";
|
|
6
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
/**
|
|
10
|
+
* `UISegmentedControl` on iOS, Material's single-choice segmented buttons on Android. A new segment plays `selection`
|
|
11
|
+
* on Android, whose buttons play nothing of their own; iOS 26's control already gives its own feedback.
|
|
12
|
+
*/
|
|
7
13
|
export const SegmentedControl = ({
|
|
8
14
|
disabled = false,
|
|
9
15
|
onChange,
|
|
@@ -13,8 +19,14 @@ export const SegmentedControl = ({
|
|
|
13
19
|
}) => {
|
|
14
20
|
const theme = useAppTheme();
|
|
15
21
|
const onNativeChange = useCallback(event => {
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
const index = event.nativeEvent.selectedSegmentIndex;
|
|
23
|
+
|
|
24
|
+
// Material reports a tap on the segment that is already selected too.
|
|
25
|
+
if (Platform.OS === 'android' && index !== selectedIndex) {
|
|
26
|
+
haptics.selection();
|
|
27
|
+
}
|
|
28
|
+
onChange?.(index, event.nativeEvent.value);
|
|
29
|
+
}, [onChange, selectedIndex]);
|
|
18
30
|
return /*#__PURE__*/_jsx(NativeSegmentedControl, {
|
|
19
31
|
appearance: theme.dark ? 'dark' : 'light',
|
|
20
32
|
enabled: !disabled,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["SegmentedControl","NativeSegmentedControl","useAppTheme","useCallback","jsx","_jsx","disabled","onChange","selectedIndex","testID","values","theme","onNativeChange","event","nativeEvent","selectedSegmentIndex","value","appearance","dark","enabled","tintColor","colorString"],"sourceRoot":"../../../../src","sources":["atoms/segmented-control/segmented-control.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,IAAIC,sBAAsB,QAAO,sCAAsC;AAC/F,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;
|
|
1
|
+
{"version":3,"names":["SegmentedControl","NativeSegmentedControl","useAppTheme","useCallback","Platform","haptics","jsx","_jsx","disabled","onChange","selectedIndex","testID","values","theme","onNativeChange","event","index","nativeEvent","selectedSegmentIndex","OS","selection","value","appearance","dark","enabled","tintColor","colorString"],"sourceRoot":"../../../../src","sources":["atoms/segmented-control/segmented-control.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,IAAIC,sBAAsB,QAAO,sCAAsC;AAC/F,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,QAAO,OAAO;AACjC,SAAQC,QAAQ,QAAO,cAAc;AAErC,SAAQC,OAAO,QAAO,wBAAe;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAGrC;AACA;AACA;AACA;AACA,OAAO,MAAMP,gBAA2C,GAAGA,CAAC;EAC1DQ,QAAQ,GAAG,KAAK;EAChBC,QAAQ;EACRC,aAAa;EACbC,MAAM;EACNC;AACqB,CAAC,KAAK;EAC3B,MAAMC,KAAK,GAAGX,WAAW,CAAC,CAAC;EAE3B,MAAMY,cAAc,GAAGX,WAAW,CAC/BY,KAAmE,IAAK;IACvE,MAAMC,KAAK,GAAGD,KAAK,CAACE,WAAW,CAACC,oBAAoB;;IAEpD;IACA,IAAId,QAAQ,CAACe,EAAE,KAAK,SAAS,IAAIH,KAAK,KAAKN,aAAa,EAAE;MACxDL,OAAO,CAACe,SAAS,CAAC,CAAC;IACrB;IAEAX,QAAQ,GAAGO,KAAK,EAAED,KAAK,CAACE,WAAW,CAACI,KAAK,CAAC;EAC5C,CAAC,EACD,CAACZ,QAAQ,EAAEC,aAAa,CAC1B,CAAC;EAED,oBACEH,IAAA,CAACN,sBAAsB;IACrBqB,UAAU,EAAET,KAAK,CAACU,IAAI,GAAG,MAAM,GAAG,OAAQ;IAC1CC,OAAO,EAAE,CAAChB,QAAS;IACnBC,QAAQ,EAAEK,cAAe;IACzBJ,aAAa,EAAEA,aAAc;IAC7BC,MAAM,EAAEA,MAAO;IACfc,SAAS,EAAEZ,KAAK,CAACa,WAAW,CAAC,iBAAiB,CAAE;IAChDd,MAAM,EAAEA;EAAO,CAChB,CAAC;AAEN,CAAC","ignoreList":[]}
|
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { Slider as ComposeSlider } from '@expo/ui/jetpack-compose';
|
|
4
4
|
import { useAppTheme } from '@hzblj/kara-theme';
|
|
5
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
6
|
+
import { haptics } from "../../haptics/index.js";
|
|
5
7
|
import { useHostGuard } from "../../host-context.js";
|
|
6
8
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
/**
|
|
10
|
+
* Material's slider. `step` is the increment, as SwiftUI takes it; Compose counts the stops between the two ends
|
|
11
|
+
* instead, so it is converted. A stepped slider ticks at every stop the thumb reaches — Compose's Material slider
|
|
12
|
+
* plays nothing of its own, where iOS 26's slider already does.
|
|
13
|
+
*/
|
|
7
14
|
export const Slider = ({
|
|
8
15
|
disabled = false,
|
|
9
16
|
max = 1,
|
|
@@ -14,6 +21,23 @@ export const Slider = ({
|
|
|
14
21
|
}) => {
|
|
15
22
|
useHostGuard('Slider');
|
|
16
23
|
const theme = useAppTheme();
|
|
24
|
+
const steps = step ? Math.max(0, Math.round((max - min) / step) - 1) : 0;
|
|
25
|
+
|
|
26
|
+
// The stop is derived from the value rather than trusting it to arrive snapped, and remembered between events:
|
|
27
|
+
// Compose reports faster than React re-renders the controlled value.
|
|
28
|
+
const stopOf = useCallback(at => step ? Math.round((at - min) / step) : 0, [min, step]);
|
|
29
|
+
const stop = useRef(stopOf(value));
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
stop.current = stopOf(value);
|
|
32
|
+
}, [stopOf, value]);
|
|
33
|
+
const onChange = useCallback(next => {
|
|
34
|
+
const reached = stopOf(next);
|
|
35
|
+
if (steps > 0 && reached !== stop.current) {
|
|
36
|
+
haptics.tick();
|
|
37
|
+
}
|
|
38
|
+
stop.current = reached;
|
|
39
|
+
onValueChange?.(next);
|
|
40
|
+
}, [onValueChange, stopOf, steps]);
|
|
17
41
|
return /*#__PURE__*/_jsx(ComposeSlider, {
|
|
18
42
|
colors: {
|
|
19
43
|
activeTrackColor: theme.color('fill.accent.primary'),
|
|
@@ -22,8 +46,8 @@ export const Slider = ({
|
|
|
22
46
|
enabled: !disabled,
|
|
23
47
|
max: max,
|
|
24
48
|
min: min,
|
|
25
|
-
onValueChange:
|
|
26
|
-
steps:
|
|
49
|
+
onValueChange: onChange,
|
|
50
|
+
steps: steps,
|
|
27
51
|
value: value
|
|
28
52
|
});
|
|
29
53
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Slider","ComposeSlider","useAppTheme","useHostGuard","jsx","_jsx","disabled","max","min","onValueChange","step","value","theme","colors","activeTrackColor","color","thumbColor","enabled"
|
|
1
|
+
{"version":3,"names":["Slider","ComposeSlider","useAppTheme","useCallback","useEffect","useRef","haptics","useHostGuard","jsx","_jsx","disabled","max","min","onValueChange","step","value","theme","steps","Math","round","stopOf","at","stop","current","onChange","next","reached","tick","colors","activeTrackColor","color","thumbColor","enabled"],"sourceRoot":"../../../../src","sources":["atoms/slider/slider.android.tsx"],"mappings":";;AAAA,SAAQA,MAAM,IAAIC,aAAa,QAAO,0BAA0B;AAChE,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,EAAEC,SAAS,EAAEC,MAAM,QAAO,OAAO;AAEpD,SAAQC,OAAO,QAAO,wBAAe;AACrC,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG/C;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMT,MAAuB,GAAGA,CAAC;EACtCU,QAAQ,GAAG,KAAK;EAChBC,GAAG,GAAG,CAAC;EACPC,GAAG,GAAG,CAAC;EACPC,aAAa;EACbC,IAAI;EACJC;AACW,CAAC,KAAK;EACjBR,YAAY,CAAC,QAAQ,CAAC;EACtB,MAAMS,KAAK,GAAGd,WAAW,CAAC,CAAC;EAC3B,MAAMe,KAAK,GAAGH,IAAI,GAAGI,IAAI,CAACP,GAAG,CAAC,CAAC,EAAEO,IAAI,CAACC,KAAK,CAAC,CAACR,GAAG,GAAGC,GAAG,IAAIE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC;;EAExE;EACA;EACA,MAAMM,MAAM,GAAGjB,WAAW,CAAEkB,EAAU,IAAMP,IAAI,GAAGI,IAAI,CAACC,KAAK,CAAC,CAACE,EAAE,GAAGT,GAAG,IAAIE,IAAI,CAAC,GAAG,CAAE,EAAE,CAACF,GAAG,EAAEE,IAAI,CAAC,CAAC;EACnG,MAAMQ,IAAI,GAAGjB,MAAM,CAACe,MAAM,CAACL,KAAK,CAAC,CAAC;EAElCX,SAAS,CAAC,MAAM;IACdkB,IAAI,CAACC,OAAO,GAAGH,MAAM,CAACL,KAAK,CAAC;EAC9B,CAAC,EAAE,CAACK,MAAM,EAAEL,KAAK,CAAC,CAAC;EAEnB,MAAMS,QAAQ,GAAGrB,WAAW,CACzBsB,IAAY,IAAK;IAChB,MAAMC,OAAO,GAAGN,MAAM,CAACK,IAAI,CAAC;IAE5B,IAAIR,KAAK,GAAG,CAAC,IAAIS,OAAO,KAAKJ,IAAI,CAACC,OAAO,EAAE;MACzCjB,OAAO,CAACqB,IAAI,CAAC,CAAC;IAChB;IAEAL,IAAI,CAACC,OAAO,GAAGG,OAAO;IACtBb,aAAa,GAAGY,IAAI,CAAC;EACvB,CAAC,EACD,CAACZ,aAAa,EAAEO,MAAM,EAAEH,KAAK,CAC/B,CAAC;EAED,oBACER,IAAA,CAACR,aAAa;IACZ2B,MAAM,EAAE;MACNC,gBAAgB,EAAEb,KAAK,CAACc,KAAK,CAAC,qBAAqB,CAAC;MACpDC,UAAU,EAAEf,KAAK,CAACc,KAAK,CAAC,qBAAqB;IAC/C,CAAE;IACFE,OAAO,EAAE,CAACtB,QAAS;IACnBC,GAAG,EAAEA,GAAI;IACTC,GAAG,EAAEA,GAAI;IACTC,aAAa,EAAEW,QAAS;IACxBP,KAAK,EAAEA,KAAM;IACbF,KAAK,EAAEA;EAAM,CACd,CAAC;AAEN,CAAC","ignoreList":[]}
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
import { Switch as ComposeSwitch, Row, Text } from '@expo/ui/jetpack-compose';
|
|
4
4
|
import { clickable, fillMaxWidth } from '@expo/ui/jetpack-compose/modifiers';
|
|
5
5
|
import { useAppTheme } from '@hzblj/kara-theme';
|
|
6
|
-
import { useMemo } from 'react';
|
|
6
|
+
import { useCallback, useMemo } from 'react';
|
|
7
|
+
import { haptics } from "../../haptics/index.js";
|
|
7
8
|
import { useHostGuard } from "../../host-context.js";
|
|
8
9
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
10
|
/**
|
|
10
11
|
* Material's switch, tinted with the accent like SwiftUI's toggle. With a label it is a full-width row — label at the
|
|
11
|
-
* start, switch at the end — and the whole row toggles, the way a Material settings row does.
|
|
12
|
+
* start, switch at the end — and the whole row toggles, the way a Material settings row does. A flip plays
|
|
13
|
+
* `toggleOn`/`toggleOff` — Android 14's constants made for switches — since Compose's Material switch plays nothing
|
|
14
|
+
* of its own. iOS stays silent, as `UISwitch` does.
|
|
12
15
|
*/
|
|
13
16
|
export const Switch = ({
|
|
14
17
|
disabled = false,
|
|
@@ -18,7 +21,11 @@ export const Switch = ({
|
|
|
18
21
|
}) => {
|
|
19
22
|
useHostGuard('Switch');
|
|
20
23
|
const theme = useAppTheme();
|
|
21
|
-
const
|
|
24
|
+
const toggle = useCallback(next => {
|
|
25
|
+
haptics.play(next ? 'toggleOn' : 'toggleOff');
|
|
26
|
+
onValueChange?.(next);
|
|
27
|
+
}, [onValueChange]);
|
|
28
|
+
const rowModifiers = useMemo(() => disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => toggle(!value))], [disabled, onValueChange, toggle, value]);
|
|
22
29
|
const control = /*#__PURE__*/_jsx(ComposeSwitch, {
|
|
23
30
|
colors: {
|
|
24
31
|
checkedBorderColor: theme.color('fill.accent.primary'),
|
|
@@ -26,7 +33,7 @@ export const Switch = ({
|
|
|
26
33
|
checkedTrackColor: theme.color('fill.accent.primary')
|
|
27
34
|
},
|
|
28
35
|
enabled: !disabled,
|
|
29
|
-
onCheckedChange: onValueChange,
|
|
36
|
+
onCheckedChange: onValueChange ? toggle : undefined,
|
|
30
37
|
value: value
|
|
31
38
|
});
|
|
32
39
|
if (!label) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Switch","ComposeSwitch","Row","Text","clickable","fillMaxWidth","useAppTheme","useMemo","useHostGuard","jsx","_jsx","jsxs","_jsxs","disabled","label","onValueChange","value","theme","rowModifiers","control","colors","checkedBorderColor","color","checkedThumbColor","checkedTrackColor","enabled","onCheckedChange","horizontalArrangement","modifiers","verticalAlignment","children","colorString","style","typography","body"],"sourceRoot":"../../../../src","sources":["atoms/switch/switch.android.tsx"],"mappings":";;AAAA,SAAQA,MAAM,IAAIC,aAAa,EAAEC,GAAG,EAAEC,IAAI,QAAO,0BAA0B;AAC3E,SAAQC,SAAS,EAAEC,YAAY,QAAO,oCAAoC;AAC1E,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,OAAO,QAAO,OAAO;
|
|
1
|
+
{"version":3,"names":["Switch","ComposeSwitch","Row","Text","clickable","fillMaxWidth","useAppTheme","useCallback","useMemo","haptics","useHostGuard","jsx","_jsx","jsxs","_jsxs","disabled","label","onValueChange","value","theme","toggle","next","play","rowModifiers","control","colors","checkedBorderColor","color","checkedThumbColor","checkedTrackColor","enabled","onCheckedChange","undefined","horizontalArrangement","modifiers","verticalAlignment","children","colorString","style","typography","body"],"sourceRoot":"../../../../src","sources":["atoms/switch/switch.android.tsx"],"mappings":";;AAAA,SAAQA,MAAM,IAAIC,aAAa,EAAEC,GAAG,EAAEC,IAAI,QAAO,0BAA0B;AAC3E,SAAQC,SAAS,EAAEC,YAAY,QAAO,oCAAoC;AAC1E,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,WAAW,EAAEC,OAAO,QAAO,OAAO;AAE1C,SAAQC,OAAO,QAAO,wBAAe;AACrC,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAG/C;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMd,MAAuB,GAAGA,CAAC;EAACe,QAAQ,GAAG,KAAK;EAAEC,KAAK;EAAEC,aAAa;EAAEC;AAAkB,CAAC,KAAK;EACvGR,YAAY,CAAC,QAAQ,CAAC;EACtB,MAAMS,KAAK,GAAGb,WAAW,CAAC,CAAC;EAC3B,MAAMc,MAAM,GAAGb,WAAW,CACvBc,IAAa,IAAK;IACjBZ,OAAO,CAACa,IAAI,CAACD,IAAI,GAAG,UAAU,GAAG,WAAW,CAAC;IAC7CJ,aAAa,GAAGI,IAAI,CAAC;EACvB,CAAC,EACD,CAACJ,aAAa,CAChB,CAAC;EACD,MAAMM,YAAY,GAAGf,OAAO,CAC1B,MAAOO,QAAQ,IAAI,CAACE,aAAa,GAAG,CAACZ,YAAY,CAAC,CAAC,CAAC,GAAG,CAACA,YAAY,CAAC,CAAC,EAAED,SAAS,CAAC,MAAMgB,MAAM,CAAC,CAACF,KAAK,CAAC,CAAC,CAAE,EACzG,CAACH,QAAQ,EAAEE,aAAa,EAAEG,MAAM,EAAEF,KAAK,CACzC,CAAC;EACD,MAAMM,OAAO,gBACXZ,IAAA,CAACX,aAAa;IACZwB,MAAM,EAAE;MACNC,kBAAkB,EAAEP,KAAK,CAACQ,KAAK,CAAC,qBAAqB,CAAC;MACtDC,iBAAiB,EAAET,KAAK,CAACQ,KAAK,CAAC,eAAe,CAAC;MAC/CE,iBAAiB,EAAEV,KAAK,CAACQ,KAAK,CAAC,qBAAqB;IACtD,CAAE;IACFG,OAAO,EAAE,CAACf,QAAS;IACnBgB,eAAe,EAAEd,aAAa,GAAGG,MAAM,GAAGY,SAAU;IACpDd,KAAK,EAAEA;EAAM,CACd,CACF;EAED,IAAI,CAACF,KAAK,EAAE;IACV,OAAOQ,OAAO;EAChB;EAEA,oBACEV,KAAA,CAACZ,GAAG;IAAC+B,qBAAqB,EAAC,cAAc;IAACC,SAAS,EAAEX,YAAa;IAACY,iBAAiB,EAAC,QAAQ;IAAAC,QAAA,gBAC3FxB,IAAA,CAACT,IAAI;MAACwB,KAAK,EAAER,KAAK,CAACkB,WAAW,CAACtB,QAAQ,GAAG,kBAAkB,GAAG,iBAAiB,CAAE;MAACuB,KAAK,EAAEnB,KAAK,CAACoB,UAAU,CAACC,IAAK;MAAAJ,QAAA,EAC7GpB;IAAK,CACF,CAAC,EACNQ,OAAO;EAAA,CACL,CAAC;AAEV,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { HAPTICS, playHaptic, prepareHaptic } from '@hzblj/kara-native';
|
|
4
|
+
let enabled = true;
|
|
5
|
+
const play = (haptic, options) => {
|
|
6
|
+
if (enabled) {
|
|
7
|
+
playHaptic(haptic, options?.intensity);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
const prepare = haptic => {
|
|
11
|
+
if (enabled) {
|
|
12
|
+
prepareHaptic(haptic);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const shorthands = Object.fromEntries(HAPTICS.map(haptic => [haptic, options => play(haptic, options)]));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The platform's own haptic feedback, named for what happened: UIKit's feedback generators on iOS, the system's
|
|
19
|
+
* `HapticFeedbackConstants` on Android — never a raw vibration. Both follow the user's system haptics setting, and
|
|
20
|
+
* every call is synchronous, with nothing to await.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* haptics.success() // a name you know: one function per haptic
|
|
24
|
+
* haptics.play(haptic) // a name that arrives as data
|
|
25
|
+
* haptics.light({intensity: 0.6}) // impacts scale on iOS
|
|
26
|
+
* haptics.prepare('selection') // iOS: warm the Taptic Engine as a gesture begins
|
|
27
|
+
* haptics.setEnabled(userPreference) // silences every kara haptic, the components' own too
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* - `selection` — a choice changed: a segment, a chip, a picker row.
|
|
31
|
+
* - `tick` — one step of a continuous gesture: a slider detent, a scrubbed value.
|
|
32
|
+
* - `toggleOn` / `toggleOff` — a switch flipped.
|
|
33
|
+
* - `success` / `warning` / `error` — how a task ended. Android has no warning of its own; it plays a rejection.
|
|
34
|
+
* - `longPress` / `dragStart` — a long press was recognised; an item lifted to be dragged.
|
|
35
|
+
* - `thresholdActivate` / `thresholdDeactivate` — a gesture crossed its commit point, or went back over it.
|
|
36
|
+
* - `keyPress` — a key of the app's own keypad.
|
|
37
|
+
* - `soft`, `light`, `medium`, `rigid`, `heavy` — a collision none of the names above describes.
|
|
38
|
+
*
|
|
39
|
+
* On Android the names that came with Android 14's constants fall back to the nearest older feedback, so every name
|
|
40
|
+
* plays on every version. Haptics are silent in a build without kara's native module.
|
|
41
|
+
*/
|
|
42
|
+
export const haptics = {
|
|
43
|
+
...shorthands,
|
|
44
|
+
/** Whether kara plays haptics — `true` until `setEnabled(false)`. */
|
|
45
|
+
isEnabled: () => enabled,
|
|
46
|
+
play,
|
|
47
|
+
prepare,
|
|
48
|
+
/**
|
|
49
|
+
* Switches every kara haptic on or off, the ones its components play included — for an app's own "Haptics" setting.
|
|
50
|
+
* Not persisted: call it at start-up with the stored preference. The system setting applies either way.
|
|
51
|
+
*/
|
|
52
|
+
setEnabled: value => {
|
|
53
|
+
enabled = value;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=haptics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["HAPTICS","playHaptic","prepareHaptic","enabled","play","haptic","options","intensity","prepare","shorthands","Object","fromEntries","map","haptics","isEnabled","setEnabled","value"],"sourceRoot":"../../../src","sources":["haptics/haptics.ts"],"mappings":";;AAAA,SAAQA,OAAO,EAAeC,UAAU,EAAEC,aAAa,QAAO,oBAAoB;AAWlF,IAAIC,OAAO,GAAG,IAAI;AAElB,MAAMC,IAAI,GAAGA,CAACC,MAAc,EAAEC,OAAuB,KAAW;EAC9D,IAAIH,OAAO,EAAE;IACXF,UAAU,CAACI,MAAM,EAAEC,OAAO,EAAEC,SAAS,CAAC;EACxC;AACF,CAAC;AAED,MAAMC,OAAO,GAAIH,MAAc,IAAW;EACxC,IAAIF,OAAO,EAAE;IACXD,aAAa,CAACG,MAAM,CAAC;EACvB;AACF,CAAC;AAED,MAAMI,UAAU,GAAGC,MAAM,CAACC,WAAW,CACnCX,OAAO,CAACY,GAAG,CAACP,MAAM,IAAI,CAACA,MAAM,EAAGC,OAAuB,IAAKF,IAAI,CAACC,MAAM,EAAEC,OAAO,CAAC,CAAC,CACpF,CAAqB;;AAErB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMO,OAAO,GAAG;EACrB,GAAGJ,UAAU;EACb;EACAK,SAAS,EAAEA,CAAA,KAAeX,OAAO;EACjCC,IAAI;EACJI,OAAO;EACP;AACF;AACA;AACA;EACEO,UAAU,EAAGC,KAAc,IAAW;IACpCb,OAAO,GAAGa,KAAK;EACjB;AACF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["haptics","useHapticFeedback"],"sourceRoot":"../../../src","sources":["haptics/index.ts"],"mappings":";;AAAA,SAAyCA,OAAO,QAAO,cAAW;AAClE,SAA6BC,iBAAiB,QAAO,0BAAuB","ignoreList":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { haptics } from "./haptics.js";
|
|
5
|
+
|
|
6
|
+
/** Picks the haptic for one change of a trigger — or none, to stay silent for that change. */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plays a haptic whenever `trigger` changes — never on mount — like SwiftUI's `sensoryFeedback(_:trigger:)`. Give it
|
|
10
|
+
* a haptic, or a function of the previous and next value that picks one per change. Changes are compared with
|
|
11
|
+
* `Object.is`, so a trigger is a primitive or a stable reference.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* useHapticFeedback(tab, 'selection')
|
|
15
|
+
* useHapticFeedback(quantity, (before, after) => (after > before ? 'tick' : 'soft'))
|
|
16
|
+
* useHapticFeedback(payment.status, (_, status) => status === 'paid' ? 'success' : status === 'failed' && 'error')
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const useHapticFeedback = (trigger, feedback) => {
|
|
20
|
+
const previous = useRef(trigger);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const before = previous.current;
|
|
23
|
+
if (Object.is(before, trigger)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
previous.current = trigger;
|
|
27
|
+
const haptic = typeof feedback === 'function' ? feedback(before, trigger) : feedback;
|
|
28
|
+
if (haptic) {
|
|
29
|
+
haptics.play(haptic);
|
|
30
|
+
}
|
|
31
|
+
}, [feedback, trigger]);
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=use-haptic-feedback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["useEffect","useRef","haptics","useHapticFeedback","trigger","feedback","previous","before","current","Object","is","haptic","play"],"sourceRoot":"../../../src","sources":["haptics/use-haptic-feedback.ts"],"mappings":";;AAAA,SAAQA,SAAS,EAAEC,MAAM,QAAO,OAAO;AAEvC,SAAqBC,OAAO,QAAO,cAAW;;AAE9C;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,iBAAiB,GAAGA,CAAIC,OAAU,EAAEC,QAAoC,KAAW;EAC9F,MAAMC,QAAQ,GAAGL,MAAM,CAACG,OAAO,CAAC;EAEhCJ,SAAS,CAAC,MAAM;IACd,MAAMO,MAAM,GAAGD,QAAQ,CAACE,OAAO;IAE/B,IAAIC,MAAM,CAACC,EAAE,CAACH,MAAM,EAAEH,OAAO,CAAC,EAAE;MAC9B;IACF;IAEAE,QAAQ,CAACE,OAAO,GAAGJ,OAAO;IAC1B,MAAMO,MAAM,GAAG,OAAON,QAAQ,KAAK,UAAU,GAAGA,QAAQ,CAACE,MAAM,EAAEH,OAAO,CAAC,GAAGC,QAAQ;IAEpF,IAAIM,MAAM,EAAE;MACVT,OAAO,CAACU,IAAI,CAACD,MAAM,CAAC;IACtB;EACF,CAAC,EAAE,CAACN,QAAQ,EAAED,OAAO,CAAC,CAAC;AACzB,CAAC","ignoreList":[]}
|
package/lib/module/index.js
CHANGED
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,cAAc,kBAAS;AACvB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,sBAAa","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,cAAc,kBAAS;AACvB,cAAc,oBAAW;AACzB,cAAc,sBAAa;AAC3B,cAAc,sBAAa;AAC3B,cAAc,sBAAa","ignoreList":[]}
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import { Text as ComposeText, DropdownMenu, DropdownMenuItem, Host } from '@expo/ui/jetpack-compose';
|
|
4
4
|
import { useSx } from '@hzblj/kara-sx';
|
|
5
5
|
import { useAppTheme } from '@hzblj/kara-theme';
|
|
6
|
-
import * as Haptics from 'expo-haptics';
|
|
7
6
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
8
7
|
import { Pressable, StyleSheet, View } from 'react-native';
|
|
9
8
|
import { Icon } from "../../atoms/icon/index.js";
|
|
9
|
+
import { haptics } from "../../haptics/index.js";
|
|
10
10
|
import { HostBoundary } from "../../host-context.js";
|
|
11
11
|
import { splitForRipple } from "../../ripple-clip.js";
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
@@ -51,7 +51,7 @@ const MenuRow = ({
|
|
|
51
51
|
* Android's own take on a context menu, since Material has no lifted preview: a long press ripples the trigger, ticks
|
|
52
52
|
* the long-press haptic, and opens a Material dropdown where the finger is — the trigger stays highlighted while the
|
|
53
53
|
* menu is up. The host behind the menu mounts on the first long press and stays, so a list of triggers that are only
|
|
54
|
-
* scrolled past never builds one. `preview` and `
|
|
54
|
+
* scrolled past never builds one. `preview`, `onPreviewPress` and `previewTransition` are iOS-only.
|
|
55
55
|
*/
|
|
56
56
|
const ContextMenuRoot = ({
|
|
57
57
|
actions,
|
|
@@ -62,6 +62,7 @@ const ContextMenuRoot = ({
|
|
|
62
62
|
const {
|
|
63
63
|
onPreviewPress: _onPreviewPress,
|
|
64
64
|
preview: _preview,
|
|
65
|
+
previewTransition: _previewTransition,
|
|
65
66
|
...sx
|
|
66
67
|
} = props;
|
|
67
68
|
const theme = useAppTheme();
|
|
@@ -86,7 +87,7 @@ const ContextMenuRoot = ({
|
|
|
86
87
|
pageX,
|
|
87
88
|
pageY
|
|
88
89
|
} = event.nativeEvent;
|
|
89
|
-
|
|
90
|
+
haptics.longPress();
|
|
90
91
|
// The touch in the trigger's own coordinates: `locationX/Y` are relative to whichever child was touched.
|
|
91
92
|
trigger.current?.measure((_x, _y, _width, _height, left, top) => {
|
|
92
93
|
setAnchor({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Text","ComposeText","DropdownMenu","DropdownMenuItem","Host","useSx","useAppTheme","
|
|
1
|
+
{"version":3,"names":["Text","ComposeText","DropdownMenu","DropdownMenuItem","Host","useSx","useAppTheme","useCallback","useMemo","useRef","useState","Pressable","StyleSheet","View","Icon","haptics","HostBoundary","splitForRipple","jsx","_jsx","jsxs","_jsxs","styles","create","anchor","height","position","width","MenuRow","action","onDismiss","theme","color","destructive","onClick","onPress","children","colorString","style","typography","body","label","icon","LeadingIcon","name","size","ContextMenuRoot","actions","testID","props","onPreviewPress","_onPreviewPress","preview","_preview","previewTransition","_previewTransition","sx","resolved","trigger","setAnchor","expanded","setExpanded","inner","outer","ripple","highlight","backgroundColor","open","event","pageX","pageY","nativeEvent","longPress","current","measure","_x","_y","_width","_height","left","top","x","y","dismiss","android_ripple","onLongPress","ref","pointerEvents","absoluteFill","colorScheme","dark","seedColor","materialSeed","onDismissRequest","Items","map","Exclusion","ContextMenu","Object","assign"],"sourceRoot":"../../../../src","sources":["organisms/context-menu/context-menu.android.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -10,6 +10,7 @@ const ContextMenuRoot = ({
|
|
|
10
10
|
children,
|
|
11
11
|
onPreviewPress,
|
|
12
12
|
preview,
|
|
13
|
+
previewTransition,
|
|
13
14
|
testID,
|
|
14
15
|
...sx
|
|
15
16
|
}) => {
|
|
@@ -28,6 +29,7 @@ const ContextMenuRoot = ({
|
|
|
28
29
|
onPreviewPress: onPreviewPress,
|
|
29
30
|
onSelectItem: onSelectItem,
|
|
30
31
|
preview: preview,
|
|
32
|
+
previewTransition: previewTransition,
|
|
31
33
|
style: style,
|
|
32
34
|
testID: testID,
|
|
33
35
|
children: children
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["iconResourceName","ContextMenu","NativeContextMenu","ContextMenuExclusion","NativeContextMenuExclusion","useSx","useCallback","useMemo","jsx","_jsx","ContextMenuRoot","actions","children","onPreviewPress","preview","testID","sx","style","items","map","action","index","destructive","id","String","image","icon","undefined","title","label","onSelectItem","Number","onPress","Exclusion","Object","assign"],"sourceRoot":"../../../../src","sources":["organisms/context-menu/context-menu.ios.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,QAAO,oBAAoB;AACnD,SAEEC,WAAW,IAAIC,iBAAiB,EAChCC,oBAAoB,IAAIC,0BAA0B,QAC7C,6BAA6B;AACpC,SAAQC,KAAK,QAAO,gBAAgB;AACpC,SAAQC,WAAW,EAAEC,OAAO,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG1C,MAAMC,eAAe,GAAGA,CAAC;
|
|
1
|
+
{"version":3,"names":["iconResourceName","ContextMenu","NativeContextMenu","ContextMenuExclusion","NativeContextMenuExclusion","useSx","useCallback","useMemo","jsx","_jsx","ContextMenuRoot","actions","children","onPreviewPress","preview","previewTransition","testID","sx","style","items","map","action","index","destructive","id","String","image","icon","undefined","title","label","onSelectItem","Number","onPress","Exclusion","Object","assign"],"sourceRoot":"../../../../src","sources":["organisms/context-menu/context-menu.ios.tsx"],"mappings":";;AAAA,SAAQA,gBAAgB,QAAO,oBAAoB;AACnD,SAEEC,WAAW,IAAIC,iBAAiB,EAChCC,oBAAoB,IAAIC,0BAA0B,QAC7C,6BAA6B;AACpC,SAAQC,KAAK,QAAO,gBAAgB;AACpC,SAAQC,WAAW,EAAEC,OAAO,QAAO,OAAO;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG1C,MAAMC,eAAe,GAAGA,CAAC;EACvBC,OAAO;EACPC,QAAQ;EACRC,cAAc;EACdC,OAAO;EACPC,iBAAiB;EACjBC,MAAM;EACN,GAAGC;AACa,CAAC,KAAK;EACtB,MAAMC,KAAK,GAAGb,KAAK,CAACY,EAAE,CAAC;;EAEvB;EACA,MAAME,KAAK,GAAGZ,OAAO,CACnB,MACEI,OAAO,CAACS,GAAG,CAAC,CAACC,MAAM,EAAEC,KAAK,MAAM;IAC9BC,WAAW,EAAEF,MAAM,CAACE,WAAW;IAC/BC,EAAE,EAAEC,MAAM,CAACH,KAAK,CAAC;IACjBI,KAAK,EAAEL,MAAM,CAACM,IAAI,GAAG3B,gBAAgB,CAACqB,MAAM,CAACM,IAAI,CAAC,GAAGC,SAAS;IAC9DC,KAAK,EAAER,MAAM,CAACS;EAChB,CAAC,CAAC,CAAC,EACL,CAACnB,OAAO,CACV,CAAC;EAED,MAAMoB,YAAY,GAAGzB,WAAW,CAAEkB,EAAU,IAAKb,OAAO,CAACqB,MAAM,CAACR,EAAE,CAAC,CAAC,EAAES,OAAO,CAAC,CAAC,EAAE,CAACtB,OAAO,CAAC,CAAC;EAE3F,oBACEF,IAAA,CAACP,iBAAiB;IAChBiB,KAAK,EAAEA,KAAM;IACbN,cAAc,EAAEA,cAAe;IAC/BkB,YAAY,EAAEA,YAAa;IAC3BjB,OAAO,EAAEA,OAAQ;IACjBC,iBAAiB,EAAEA,iBAAkB;IACrCG,KAAK,EAAEA,KAAM;IACbF,MAAM,EAAEA,MAAO;IAAAJ,QAAA,EAEdA;EAAQ,CACQ,CAAC;AAExB,CAAC;AAED,MAAMsB,SAAS,GAAGA,CAAC;EAACtB;AAAmC,CAAC,kBACtDH,IAAA,CAACL,0BAA0B;EAAAQ,QAAA,EAAEA;AAAQ,CAA6B,CACnE;AAED,OAAO,MAAMX,WAAiC,GAAGkC,MAAM,CAACC,MAAM,CAAC1B,eAAe,EAAE;EAACwB;AAAS,CAAC,CAAC","ignoreList":[]}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ResponsiveViewSx } from '@hzblj/kara-sx';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
+
import type { Haptic } from '../../haptics';
|
|
3
4
|
export type PressableProps = ResponsiveViewSx & {
|
|
4
5
|
children?: ReactNode;
|
|
5
6
|
disabled?: boolean;
|
|
6
|
-
haptic
|
|
7
|
+
/** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
|
|
8
|
+
haptic?: boolean | Haptic;
|
|
7
9
|
onPress?: () => void;
|
|
8
10
|
testID?: string;
|
|
9
11
|
};
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { SegmentedControlComponent } from './segmented-control.types';
|
|
2
|
+
/**
|
|
3
|
+
* `UISegmentedControl` on iOS, Material's single-choice segmented buttons on Android. A new segment plays `selection`
|
|
4
|
+
* on Android, whose buttons play nothing of their own; iOS 26's control already gives its own feedback.
|
|
5
|
+
*/
|
|
2
6
|
export declare const SegmentedControl: SegmentedControlComponent;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Haptic } from '@hzblj/kara-native';
|
|
2
|
+
export type { Haptic };
|
|
3
|
+
export type HapticOptions = {
|
|
4
|
+
/** iOS: scales an impact — `soft`, `light`, `medium`, `rigid`, `heavy` — from 0 to 1. Everything else ignores it. */
|
|
5
|
+
intensity?: number;
|
|
6
|
+
};
|
|
7
|
+
declare const play: (haptic: Haptic, options?: HapticOptions) => void;
|
|
8
|
+
declare const prepare: (haptic: Haptic) => void;
|
|
9
|
+
/**
|
|
10
|
+
* The platform's own haptic feedback, named for what happened: UIKit's feedback generators on iOS, the system's
|
|
11
|
+
* `HapticFeedbackConstants` on Android — never a raw vibration. Both follow the user's system haptics setting, and
|
|
12
|
+
* every call is synchronous, with nothing to await.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* haptics.success() // a name you know: one function per haptic
|
|
16
|
+
* haptics.play(haptic) // a name that arrives as data
|
|
17
|
+
* haptics.light({intensity: 0.6}) // impacts scale on iOS
|
|
18
|
+
* haptics.prepare('selection') // iOS: warm the Taptic Engine as a gesture begins
|
|
19
|
+
* haptics.setEnabled(userPreference) // silences every kara haptic, the components' own too
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* - `selection` — a choice changed: a segment, a chip, a picker row.
|
|
23
|
+
* - `tick` — one step of a continuous gesture: a slider detent, a scrubbed value.
|
|
24
|
+
* - `toggleOn` / `toggleOff` — a switch flipped.
|
|
25
|
+
* - `success` / `warning` / `error` — how a task ended. Android has no warning of its own; it plays a rejection.
|
|
26
|
+
* - `longPress` / `dragStart` — a long press was recognised; an item lifted to be dragged.
|
|
27
|
+
* - `thresholdActivate` / `thresholdDeactivate` — a gesture crossed its commit point, or went back over it.
|
|
28
|
+
* - `keyPress` — a key of the app's own keypad.
|
|
29
|
+
* - `soft`, `light`, `medium`, `rigid`, `heavy` — a collision none of the names above describes.
|
|
30
|
+
*
|
|
31
|
+
* On Android the names that came with Android 14's constants fall back to the nearest older feedback, so every name
|
|
32
|
+
* plays on every version. Haptics are silent in a build without kara's native module.
|
|
33
|
+
*/
|
|
34
|
+
export declare const haptics: {
|
|
35
|
+
/** Whether kara plays haptics — `true` until `setEnabled(false)`. */
|
|
36
|
+
isEnabled: () => boolean;
|
|
37
|
+
play: typeof play;
|
|
38
|
+
prepare: typeof prepare;
|
|
39
|
+
/**
|
|
40
|
+
* Switches every kara haptic on or off, the ones its components play included — for an app's own "Haptics" setting.
|
|
41
|
+
* Not persisted: call it at start-up with the stored preference. The system setting applies either way.
|
|
42
|
+
*/
|
|
43
|
+
setEnabled: (value: boolean) => void;
|
|
44
|
+
dragStart: (options?: HapticOptions) => void;
|
|
45
|
+
error: (options?: HapticOptions) => void;
|
|
46
|
+
heavy: (options?: HapticOptions) => void;
|
|
47
|
+
keyPress: (options?: HapticOptions) => void;
|
|
48
|
+
light: (options?: HapticOptions) => void;
|
|
49
|
+
longPress: (options?: HapticOptions) => void;
|
|
50
|
+
medium: (options?: HapticOptions) => void;
|
|
51
|
+
rigid: (options?: HapticOptions) => void;
|
|
52
|
+
selection: (options?: HapticOptions) => void;
|
|
53
|
+
soft: (options?: HapticOptions) => void;
|
|
54
|
+
success: (options?: HapticOptions) => void;
|
|
55
|
+
thresholdActivate: (options?: HapticOptions) => void;
|
|
56
|
+
thresholdDeactivate: (options?: HapticOptions) => void;
|
|
57
|
+
tick: (options?: HapticOptions) => void;
|
|
58
|
+
toggleOff: (options?: HapticOptions) => void;
|
|
59
|
+
toggleOn: (options?: HapticOptions) => void;
|
|
60
|
+
warning: (options?: HapticOptions) => void;
|
|
61
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Haptic } from './haptics';
|
|
2
|
+
/** Picks the haptic for one change of a trigger — or none, to stay silent for that change. */
|
|
3
|
+
export type HapticResolver<T> = (previous: T, next: T) => Haptic | false | null | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Plays a haptic whenever `trigger` changes — never on mount — like SwiftUI's `sensoryFeedback(_:trigger:)`. Give it
|
|
6
|
+
* a haptic, or a function of the previous and next value that picks one per change. Changes are compared with
|
|
7
|
+
* `Object.is`, so a trigger is a primitive or a stable reference.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* useHapticFeedback(tab, 'selection')
|
|
11
|
+
* useHapticFeedback(quantity, (before, after) => (after > before ? 'tick' : 'soft'))
|
|
12
|
+
* useHapticFeedback(payment.status, (_, status) => status === 'paid' ? 'success' : status === 'failed' && 'error')
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const useHapticFeedback: <T>(trigger: T, feedback: Haptic | HapticResolver<T>) => void;
|
|
@@ -20,6 +20,14 @@ export type ContextMenuProps = ResponsiveViewSx & {
|
|
|
20
20
|
* every preview up front.
|
|
21
21
|
*/
|
|
22
22
|
preview?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* iOS: how the preview leaves once tapped, before `onPreviewPress`. `zoom`: its photo grows straight into the pushed
|
|
25
|
+
* screen, the way the App Store expands a preview — needs a `ZoomTransition.Source` (`@hzblj/kara-ui/router`) in the
|
|
26
|
+
* trigger, a `preview` and iOS 18, and shrinks back like `dismiss` otherwise. `pop`: UIKit's default, the preview
|
|
27
|
+
* flies out. `dismiss`: it shrinks back onto the trigger first. Default: `zoom` when the trigger holds a
|
|
28
|
+
* `ZoomTransition.Source`, `pop` otherwise.
|
|
29
|
+
*/
|
|
30
|
+
previewTransition?: 'dismiss' | 'pop' | 'zoom';
|
|
23
31
|
testID?: string;
|
|
24
32
|
};
|
|
25
33
|
export type ContextMenuExclusionProps = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hzblj/kara-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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,9 @@
|
|
|
59
59
|
]
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@hzblj/kara-native": "^0.
|
|
63
|
-
"@hzblj/kara-sx": "^0.
|
|
64
|
-
"@hzblj/kara-theme": "^0.
|
|
62
|
+
"@hzblj/kara-native": "^0.6.0",
|
|
63
|
+
"@hzblj/kara-sx": "^0.6.0",
|
|
64
|
+
"@hzblj/kara-theme": "^0.6.0",
|
|
65
65
|
"sf-symbols-typescript": "^2.2.0"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
"@lodev09/react-native-true-sheet": ">=3.11.0",
|
|
70
70
|
"@shopify/flash-list": ">=2.0.0",
|
|
71
71
|
"expo": "~57.0.0",
|
|
72
|
-
"expo-haptics": "*",
|
|
73
72
|
"expo-image": "*",
|
|
74
73
|
"react": "*",
|
|
75
74
|
"react-native": "*",
|
|
@@ -82,7 +81,6 @@
|
|
|
82
81
|
"@storybook/react-native": "^10.6.0",
|
|
83
82
|
"@types/react": "~19.2.18",
|
|
84
83
|
"expo": "~57.0.22",
|
|
85
|
-
"expo-haptics": "~57.0.3",
|
|
86
84
|
"expo-image": "~57.0.5",
|
|
87
85
|
"react": "19.2.3",
|
|
88
86
|
"react-native": "0.86.3",
|
package/src/atoms/chip/chip.tsx
CHANGED
|
@@ -9,7 +9,7 @@ export const Chip: ChipComponent = ({label, onPress, selected = false, testID}:
|
|
|
9
9
|
borderColor={selected ? 'border.accent' : 'border.tertiary'}
|
|
10
10
|
borderRadius="full"
|
|
11
11
|
borderWidth={1}
|
|
12
|
-
haptic
|
|
12
|
+
haptic="selection"
|
|
13
13
|
justify="center"
|
|
14
14
|
onPress={onPress}
|
|
15
15
|
px="s12"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {useSplitSx} from '@hzblj/kara-sx'
|
|
2
|
-
import * as Haptics from 'expo-haptics'
|
|
3
2
|
import {useCallback} from 'react'
|
|
4
3
|
import {Pressable as RNPressable} from 'react-native'
|
|
5
4
|
import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'
|
|
5
|
+
|
|
6
|
+
import {haptics} from '../../haptics'
|
|
6
7
|
import type {PressableComponent, PressableProps} from './pressable.types'
|
|
7
8
|
|
|
8
9
|
const SPRING = {damping: 20, stiffness: 400}
|
|
@@ -19,24 +20,30 @@ export const Pressable: PressableComponent = ({
|
|
|
19
20
|
}: PressableProps) => {
|
|
20
21
|
const [resolved, rest] = useSplitSx(props)
|
|
21
22
|
const scale = useSharedValue(1)
|
|
23
|
+
const feedback = haptic === true ? 'light' : haptic || undefined
|
|
22
24
|
|
|
23
25
|
const rStyle = useAnimatedStyle(() => ({transform: [{scale: scale.value}]}), [])
|
|
24
26
|
|
|
27
|
+
// The finger is down: the Taptic Engine has until the release to wake, so the haptic lands with the press.
|
|
25
28
|
const onPressIn = useCallback(() => {
|
|
26
29
|
scale.value = withSpring(0.96, SPRING)
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
if (feedback) {
|
|
32
|
+
haptics.prepare(feedback)
|
|
33
|
+
}
|
|
34
|
+
}, [feedback, scale])
|
|
28
35
|
|
|
29
36
|
const onPressOut = useCallback(() => {
|
|
30
37
|
scale.value = withSpring(1, SPRING)
|
|
31
38
|
}, [scale])
|
|
32
39
|
|
|
33
40
|
const onPressWithHaptic = useCallback(() => {
|
|
34
|
-
if (
|
|
35
|
-
|
|
41
|
+
if (feedback) {
|
|
42
|
+
haptics.play(feedback)
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
onPress?.()
|
|
39
|
-
}, [
|
|
46
|
+
}, [feedback, onPress])
|
|
40
47
|
|
|
41
48
|
return (
|
|
42
49
|
<AnimatedPressable
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type {ResponsiveViewSx} from '@hzblj/kara-sx'
|
|
2
2
|
import type {ReactNode} from 'react'
|
|
3
3
|
|
|
4
|
+
import type {Haptic} from '../../haptics'
|
|
5
|
+
|
|
4
6
|
export type PressableProps = ResponsiveViewSx & {
|
|
5
7
|
children?: ReactNode
|
|
6
8
|
disabled?: boolean
|
|
7
|
-
haptic
|
|
9
|
+
/** Played on press. `true` is a `light` impact; a name picks the haptic, e.g. `selection` for a choice. */
|
|
10
|
+
haptic?: boolean | Haptic
|
|
8
11
|
onPress?: () => void
|
|
9
12
|
testID?: string
|
|
10
13
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {SegmentedControl as NativeSegmentedControl} from '@expo/ui/community/segmented-control'
|
|
2
2
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
3
|
import {useCallback} from 'react'
|
|
4
|
+
import {Platform} from 'react-native'
|
|
4
5
|
|
|
6
|
+
import {haptics} from '../../haptics'
|
|
5
7
|
import type {SegmentedControlComponent, SegmentedControlProps} from './segmented-control.types'
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* `UISegmentedControl` on iOS, Material's single-choice segmented buttons on Android. A new segment plays `selection`
|
|
11
|
+
* on Android, whose buttons play nothing of their own; iOS 26's control already gives its own feedback.
|
|
12
|
+
*/
|
|
7
13
|
export const SegmentedControl: SegmentedControlComponent = ({
|
|
8
14
|
disabled = false,
|
|
9
15
|
onChange,
|
|
@@ -15,9 +21,16 @@ export const SegmentedControl: SegmentedControlComponent = ({
|
|
|
15
21
|
|
|
16
22
|
const onNativeChange = useCallback(
|
|
17
23
|
(event: {nativeEvent: {selectedSegmentIndex: number; value: string}}) => {
|
|
18
|
-
|
|
24
|
+
const index = event.nativeEvent.selectedSegmentIndex
|
|
25
|
+
|
|
26
|
+
// Material reports a tap on the segment that is already selected too.
|
|
27
|
+
if (Platform.OS === 'android' && index !== selectedIndex) {
|
|
28
|
+
haptics.selection()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onChange?.(index, event.nativeEvent.value)
|
|
19
32
|
},
|
|
20
|
-
[onChange]
|
|
33
|
+
[onChange, selectedIndex]
|
|
21
34
|
)
|
|
22
35
|
|
|
23
36
|
return (
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {Slider as ComposeSlider} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
3
|
+
import {useCallback, useEffect, useRef} from 'react'
|
|
3
4
|
|
|
5
|
+
import {haptics} from '../../haptics'
|
|
4
6
|
import {useHostGuard} from '../../host-context'
|
|
5
7
|
import type {SliderComponent, SliderProps} from './slider.types'
|
|
6
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Material's slider. `step` is the increment, as SwiftUI takes it; Compose counts the stops between the two ends
|
|
11
|
+
* instead, so it is converted. A stepped slider ticks at every stop the thumb reaches — Compose's Material slider
|
|
12
|
+
* plays nothing of its own, where iOS 26's slider already does.
|
|
13
|
+
*/
|
|
7
14
|
export const Slider: SliderComponent = ({
|
|
8
15
|
disabled = false,
|
|
9
16
|
max = 1,
|
|
@@ -14,6 +21,30 @@ export const Slider: SliderComponent = ({
|
|
|
14
21
|
}: SliderProps) => {
|
|
15
22
|
useHostGuard('Slider')
|
|
16
23
|
const theme = useAppTheme()
|
|
24
|
+
const steps = step ? Math.max(0, Math.round((max - min) / step) - 1) : 0
|
|
25
|
+
|
|
26
|
+
// The stop is derived from the value rather than trusting it to arrive snapped, and remembered between events:
|
|
27
|
+
// Compose reports faster than React re-renders the controlled value.
|
|
28
|
+
const stopOf = useCallback((at: number) => (step ? Math.round((at - min) / step) : 0), [min, step])
|
|
29
|
+
const stop = useRef(stopOf(value))
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
stop.current = stopOf(value)
|
|
33
|
+
}, [stopOf, value])
|
|
34
|
+
|
|
35
|
+
const onChange = useCallback(
|
|
36
|
+
(next: number) => {
|
|
37
|
+
const reached = stopOf(next)
|
|
38
|
+
|
|
39
|
+
if (steps > 0 && reached !== stop.current) {
|
|
40
|
+
haptics.tick()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
stop.current = reached
|
|
44
|
+
onValueChange?.(next)
|
|
45
|
+
},
|
|
46
|
+
[onValueChange, stopOf, steps]
|
|
47
|
+
)
|
|
17
48
|
|
|
18
49
|
return (
|
|
19
50
|
<ComposeSlider
|
|
@@ -24,8 +55,8 @@ export const Slider: SliderComponent = ({
|
|
|
24
55
|
enabled={!disabled}
|
|
25
56
|
max={max}
|
|
26
57
|
min={min}
|
|
27
|
-
onValueChange={
|
|
28
|
-
steps={
|
|
58
|
+
onValueChange={onChange}
|
|
59
|
+
steps={steps}
|
|
29
60
|
value={value}
|
|
30
61
|
/>
|
|
31
62
|
)
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import {Switch as ComposeSwitch, Row, Text} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {clickable, fillMaxWidth} from '@expo/ui/jetpack-compose/modifiers'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
-
import {useMemo} from 'react'
|
|
4
|
+
import {useCallback, useMemo} from 'react'
|
|
5
5
|
|
|
6
|
+
import {haptics} from '../../haptics'
|
|
6
7
|
import {useHostGuard} from '../../host-context'
|
|
7
8
|
import type {SwitchComponent, SwitchProps} from './switch.types'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Material's switch, tinted with the accent like SwiftUI's toggle. With a label it is a full-width row — label at the
|
|
11
|
-
* start, switch at the end — and the whole row toggles, the way a Material settings row does.
|
|
12
|
+
* start, switch at the end — and the whole row toggles, the way a Material settings row does. A flip plays
|
|
13
|
+
* `toggleOn`/`toggleOff` — Android 14's constants made for switches — since Compose's Material switch plays nothing
|
|
14
|
+
* of its own. iOS stays silent, as `UISwitch` does.
|
|
12
15
|
*/
|
|
13
16
|
export const Switch: SwitchComponent = ({disabled = false, label, onValueChange, value}: SwitchProps) => {
|
|
14
17
|
useHostGuard('Switch')
|
|
15
18
|
const theme = useAppTheme()
|
|
19
|
+
const toggle = useCallback(
|
|
20
|
+
(next: boolean) => {
|
|
21
|
+
haptics.play(next ? 'toggleOn' : 'toggleOff')
|
|
22
|
+
onValueChange?.(next)
|
|
23
|
+
},
|
|
24
|
+
[onValueChange]
|
|
25
|
+
)
|
|
16
26
|
const rowModifiers = useMemo(
|
|
17
|
-
() => (disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() =>
|
|
18
|
-
[disabled, onValueChange, value]
|
|
27
|
+
() => (disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => toggle(!value))]),
|
|
28
|
+
[disabled, onValueChange, toggle, value]
|
|
19
29
|
)
|
|
20
30
|
const control = (
|
|
21
31
|
<ComposeSwitch
|
|
@@ -25,7 +35,7 @@ export const Switch: SwitchComponent = ({disabled = false, label, onValueChange,
|
|
|
25
35
|
checkedTrackColor: theme.color('fill.accent.primary'),
|
|
26
36
|
}}
|
|
27
37
|
enabled={!disabled}
|
|
28
|
-
onCheckedChange={onValueChange}
|
|
38
|
+
onCheckedChange={onValueChange ? toggle : undefined}
|
|
29
39
|
value={value}
|
|
30
40
|
/>
|
|
31
41
|
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {HAPTICS, type Haptic, playHaptic, prepareHaptic} from '@hzblj/kara-native'
|
|
2
|
+
|
|
3
|
+
export type {Haptic}
|
|
4
|
+
|
|
5
|
+
export type HapticOptions = {
|
|
6
|
+
/** iOS: scales an impact — `soft`, `light`, `medium`, `rigid`, `heavy` — from 0 to 1. Everything else ignores it. */
|
|
7
|
+
intensity?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type HapticShorthands = {readonly [K in Haptic]: (options?: HapticOptions) => void}
|
|
11
|
+
|
|
12
|
+
let enabled = true
|
|
13
|
+
|
|
14
|
+
const play = (haptic: Haptic, options?: HapticOptions): void => {
|
|
15
|
+
if (enabled) {
|
|
16
|
+
playHaptic(haptic, options?.intensity)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const prepare = (haptic: Haptic): void => {
|
|
21
|
+
if (enabled) {
|
|
22
|
+
prepareHaptic(haptic)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const shorthands = Object.fromEntries(
|
|
27
|
+
HAPTICS.map(haptic => [haptic, (options?: HapticOptions) => play(haptic, options)])
|
|
28
|
+
) as HapticShorthands
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The platform's own haptic feedback, named for what happened: UIKit's feedback generators on iOS, the system's
|
|
32
|
+
* `HapticFeedbackConstants` on Android — never a raw vibration. Both follow the user's system haptics setting, and
|
|
33
|
+
* every call is synchronous, with nothing to await.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* haptics.success() // a name you know: one function per haptic
|
|
37
|
+
* haptics.play(haptic) // a name that arrives as data
|
|
38
|
+
* haptics.light({intensity: 0.6}) // impacts scale on iOS
|
|
39
|
+
* haptics.prepare('selection') // iOS: warm the Taptic Engine as a gesture begins
|
|
40
|
+
* haptics.setEnabled(userPreference) // silences every kara haptic, the components' own too
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* - `selection` — a choice changed: a segment, a chip, a picker row.
|
|
44
|
+
* - `tick` — one step of a continuous gesture: a slider detent, a scrubbed value.
|
|
45
|
+
* - `toggleOn` / `toggleOff` — a switch flipped.
|
|
46
|
+
* - `success` / `warning` / `error` — how a task ended. Android has no warning of its own; it plays a rejection.
|
|
47
|
+
* - `longPress` / `dragStart` — a long press was recognised; an item lifted to be dragged.
|
|
48
|
+
* - `thresholdActivate` / `thresholdDeactivate` — a gesture crossed its commit point, or went back over it.
|
|
49
|
+
* - `keyPress` — a key of the app's own keypad.
|
|
50
|
+
* - `soft`, `light`, `medium`, `rigid`, `heavy` — a collision none of the names above describes.
|
|
51
|
+
*
|
|
52
|
+
* On Android the names that came with Android 14's constants fall back to the nearest older feedback, so every name
|
|
53
|
+
* plays on every version. Haptics are silent in a build without kara's native module.
|
|
54
|
+
*/
|
|
55
|
+
export const haptics = {
|
|
56
|
+
...shorthands,
|
|
57
|
+
/** Whether kara plays haptics — `true` until `setEnabled(false)`. */
|
|
58
|
+
isEnabled: (): boolean => enabled,
|
|
59
|
+
play,
|
|
60
|
+
prepare,
|
|
61
|
+
/**
|
|
62
|
+
* Switches every kara haptic on or off, the ones its components play included — for an app's own "Haptics" setting.
|
|
63
|
+
* Not persisted: call it at start-up with the stored preference. The system setting applies either way.
|
|
64
|
+
*/
|
|
65
|
+
setEnabled: (value: boolean): void => {
|
|
66
|
+
enabled = value
|
|
67
|
+
},
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {useEffect, useRef} from 'react'
|
|
2
|
+
|
|
3
|
+
import {type Haptic, haptics} from './haptics'
|
|
4
|
+
|
|
5
|
+
/** Picks the haptic for one change of a trigger — or none, to stay silent for that change. */
|
|
6
|
+
export type HapticResolver<T> = (previous: T, next: T) => Haptic | false | null | undefined
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plays a haptic whenever `trigger` changes — never on mount — like SwiftUI's `sensoryFeedback(_:trigger:)`. Give it
|
|
10
|
+
* a haptic, or a function of the previous and next value that picks one per change. Changes are compared with
|
|
11
|
+
* `Object.is`, so a trigger is a primitive or a stable reference.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* useHapticFeedback(tab, 'selection')
|
|
15
|
+
* useHapticFeedback(quantity, (before, after) => (after > before ? 'tick' : 'soft'))
|
|
16
|
+
* useHapticFeedback(payment.status, (_, status) => status === 'paid' ? 'success' : status === 'failed' && 'error')
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const useHapticFeedback = <T>(trigger: T, feedback: Haptic | HapticResolver<T>): void => {
|
|
20
|
+
const previous = useRef(trigger)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const before = previous.current
|
|
24
|
+
|
|
25
|
+
if (Object.is(before, trigger)) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
previous.current = trigger
|
|
30
|
+
const haptic = typeof feedback === 'function' ? feedback(before, trigger) : feedback
|
|
31
|
+
|
|
32
|
+
if (haptic) {
|
|
33
|
+
haptics.play(haptic)
|
|
34
|
+
}
|
|
35
|
+
}, [feedback, trigger])
|
|
36
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {Text as ComposeText, DropdownMenu, DropdownMenuItem, Host} from '@expo/ui/jetpack-compose'
|
|
2
2
|
import {useSx} from '@hzblj/kara-sx'
|
|
3
3
|
import {useAppTheme} from '@hzblj/kara-theme'
|
|
4
|
-
import * as Haptics from 'expo-haptics'
|
|
5
4
|
import {useCallback, useMemo, useRef, useState} from 'react'
|
|
6
5
|
import {type GestureResponderEvent, Pressable, StyleSheet, View} from 'react-native'
|
|
7
6
|
import {Icon} from '../../atoms/icon'
|
|
7
|
+
import {haptics} from '../../haptics'
|
|
8
8
|
import {HostBoundary} from '../../host-context'
|
|
9
9
|
import {splitForRipple} from '../../ripple-clip'
|
|
10
10
|
import type {MenuAction} from '../menu'
|
|
@@ -52,10 +52,10 @@ const MenuRow = ({action, onDismiss}: MenuRowProps) => {
|
|
|
52
52
|
* Android's own take on a context menu, since Material has no lifted preview: a long press ripples the trigger, ticks
|
|
53
53
|
* the long-press haptic, and opens a Material dropdown where the finger is — the trigger stays highlighted while the
|
|
54
54
|
* menu is up. The host behind the menu mounts on the first long press and stays, so a list of triggers that are only
|
|
55
|
-
* scrolled past never builds one. `preview` and `
|
|
55
|
+
* scrolled past never builds one. `preview`, `onPreviewPress` and `previewTransition` are iOS-only.
|
|
56
56
|
*/
|
|
57
57
|
const ContextMenuRoot = ({actions, children, testID, ...props}: ContextMenuProps) => {
|
|
58
|
-
const {onPreviewPress: _onPreviewPress, preview: _preview, ...sx} = props
|
|
58
|
+
const {onPreviewPress: _onPreviewPress, preview: _preview, previewTransition: _previewTransition, ...sx} = props
|
|
59
59
|
const theme = useAppTheme()
|
|
60
60
|
const resolved = useSx(sx)
|
|
61
61
|
const trigger = useRef<View>(null)
|
|
@@ -70,7 +70,7 @@ const ContextMenuRoot = ({actions, children, testID, ...props}: ContextMenuProps
|
|
|
70
70
|
const open = useCallback((event: GestureResponderEvent) => {
|
|
71
71
|
const {pageX, pageY} = event.nativeEvent
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
haptics.longPress()
|
|
74
74
|
// The touch in the trigger's own coordinates: `locationX/Y` are relative to whichever child was touched.
|
|
75
75
|
trigger.current?.measure((_x, _y, _width, _height, left, top) => {
|
|
76
76
|
setAnchor({x: pageX - left, y: pageY - top})
|
|
@@ -8,7 +8,15 @@ import {useSx} from '@hzblj/kara-sx'
|
|
|
8
8
|
import {useCallback, useMemo} from 'react'
|
|
9
9
|
import type {ContextMenuComponent, ContextMenuExclusionProps, ContextMenuProps} from './context-menu.types'
|
|
10
10
|
|
|
11
|
-
const ContextMenuRoot = ({
|
|
11
|
+
const ContextMenuRoot = ({
|
|
12
|
+
actions,
|
|
13
|
+
children,
|
|
14
|
+
onPreviewPress,
|
|
15
|
+
preview,
|
|
16
|
+
previewTransition,
|
|
17
|
+
testID,
|
|
18
|
+
...sx
|
|
19
|
+
}: ContextMenuProps) => {
|
|
12
20
|
const style = useSx(sx)
|
|
13
21
|
|
|
14
22
|
/** The native side hands back an id; the index is the one identity every action has. */
|
|
@@ -31,6 +39,7 @@ const ContextMenuRoot = ({actions, children, onPreviewPress, preview, testID, ..
|
|
|
31
39
|
onPreviewPress={onPreviewPress}
|
|
32
40
|
onSelectItem={onSelectItem}
|
|
33
41
|
preview={preview}
|
|
42
|
+
previewTransition={previewTransition}
|
|
34
43
|
style={style}
|
|
35
44
|
testID={testID}
|
|
36
45
|
>
|
|
@@ -21,6 +21,14 @@ export type ContextMenuProps = ResponsiveViewSx & {
|
|
|
21
21
|
* every preview up front.
|
|
22
22
|
*/
|
|
23
23
|
preview?: ReactNode
|
|
24
|
+
/**
|
|
25
|
+
* iOS: how the preview leaves once tapped, before `onPreviewPress`. `zoom`: its photo grows straight into the pushed
|
|
26
|
+
* screen, the way the App Store expands a preview — needs a `ZoomTransition.Source` (`@hzblj/kara-ui/router`) in the
|
|
27
|
+
* trigger, a `preview` and iOS 18, and shrinks back like `dismiss` otherwise. `pop`: UIKit's default, the preview
|
|
28
|
+
* flies out. `dismiss`: it shrinks back onto the trigger first. Default: `zoom` when the trigger holds a
|
|
29
|
+
* `ZoomTransition.Source`, `pop` otherwise.
|
|
30
|
+
*/
|
|
31
|
+
previewTransition?: 'dismiss' | 'pop' | 'zoom'
|
|
24
32
|
testID?: string
|
|
25
33
|
}
|
|
26
34
|
|