@hzblj/kara-components 0.5.0 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/lib/module/atoms/chip/chip.js +1 -1
  3. package/lib/module/atoms/chip/chip.js.map +1 -1
  4. package/lib/module/atoms/pressable/pressable.js +11 -5
  5. package/lib/module/atoms/pressable/pressable.js.map +1 -1
  6. package/lib/module/atoms/segmented-control/segmented-control.js +14 -2
  7. package/lib/module/atoms/segmented-control/segmented-control.js.map +1 -1
  8. package/lib/module/atoms/slider/slider.android.js +26 -2
  9. package/lib/module/atoms/slider/slider.android.js.map +1 -1
  10. package/lib/module/atoms/switch/switch.android.js +11 -4
  11. package/lib/module/atoms/switch/switch.android.js.map +1 -1
  12. package/lib/module/haptics/haptics.js +56 -0
  13. package/lib/module/haptics/haptics.js.map +1 -0
  14. package/lib/module/haptics/index.js +5 -0
  15. package/lib/module/haptics/index.js.map +1 -0
  16. package/lib/module/haptics/use-haptic-feedback.js +33 -0
  17. package/lib/module/haptics/use-haptic-feedback.js.map +1 -0
  18. package/lib/module/index.js +1 -0
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/organisms/context-menu/context-menu.android.js +2 -2
  21. package/lib/module/organisms/context-menu/context-menu.android.js.map +1 -1
  22. package/lib/typescript/atoms/pressable/pressable.types.d.ts +3 -1
  23. package/lib/typescript/atoms/segmented-control/segmented-control.d.ts +4 -0
  24. package/lib/typescript/haptics/haptics.d.ts +61 -0
  25. package/lib/typescript/haptics/index.d.ts +2 -0
  26. package/lib/typescript/haptics/use-haptic-feedback.d.ts +15 -0
  27. package/lib/typescript/index.d.ts +1 -0
  28. package/package.json +4 -6
  29. package/src/atoms/chip/chip.tsx +1 -1
  30. package/src/atoms/pressable/pressable.tsx +12 -5
  31. package/src/atoms/pressable/pressable.types.ts +4 -1
  32. package/src/atoms/segmented-control/segmented-control.tsx +15 -2
  33. package/src/atoms/slider/slider.android.tsx +33 -2
  34. package/src/atoms/switch/switch.android.tsx +15 -5
  35. package/src/haptics/haptics.ts +68 -0
  36. package/src/haptics/index.ts +2 -0
  37. package/src/haptics/use-haptic-feedback.ts +36 -0
  38. package/src/index.ts +1 -0
  39. package/src/organisms/context-menu/context-menu.android.tsx +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
3
25
  ## 0.5.0
4
26
 
5
27
  ### Minor Changes
@@ -14,7 +14,7 @@ export const Chip = ({
14
14
  borderColor: selected ? 'border.accent' : 'border.tertiary',
15
15
  borderRadius: "full",
16
16
  borderWidth: 1,
17
- haptic: true,
17
+ haptic: "selection",
18
18
  justify: "center",
19
19
  onPress: onPress,
20
20
  px: "s12",
@@ -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;EACNC,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
+ {"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
- }, [scale]);
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 (haptic) {
37
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => undefined);
42
+ if (feedback) {
43
+ haptics.play(feedback);
38
44
  }
39
45
  onPress?.();
40
- }, [haptic, onPress]);
46
+ }, [feedback, onPress]);
41
47
  return /*#__PURE__*/_jsx(AnimatedPressable, {
42
48
  disabled: disabled,
43
49
  hitSlop: 8,
@@ -1 +1 @@
1
- {"version":3,"names":["useSplitSx","Haptics","useCallback","Pressable","RNPressable","Animated","useAnimatedStyle","useSharedValue","withSpring","jsx","_jsx","SPRING","damping","stiffness","AnimatedPressable","createAnimatedComponent","children","disabled","haptic","onPress","testID","props","resolved","rest","scale","rStyle","transform","value","onPressIn","onPressOut","onPressWithHaptic","impactAsync","ImpactFeedbackStyle","Light","catch","undefined","hitSlop","style"],"sourceRoot":"../../../../src","sources":["atoms/pressable/pressable.tsx"],"mappings":";;AAAA,SAAQA,UAAU,QAAO,gBAAgB;AACzC,OAAO,KAAKC,OAAO,MAAM,cAAc;AACvC,SAAQC,WAAW,QAAO,OAAO;AACjC,SAAQC,SAAS,IAAIC,WAAW,QAAO,cAAc;AACrD,OAAOC,QAAQ,IAAGC,gBAAgB,EAAEC,cAAc,EAAEC,UAAU,QAAO,yBAAyB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG9F,MAAMC,MAAM,GAAG;EAACC,OAAO,EAAE,EAAE;EAAEC,SAAS,EAAE;AAAG,CAAC;AAE5C,MAAMC,iBAAiB,GAAGT,QAAQ,CAACU,uBAAuB,CAACX,WAAW,CAAC;AAEvE,OAAO,MAAMD,SAA6B,GAAGA,CAAC;EAC5Ca,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,GAAGjB,cAAc,CAAC,CAAC,CAAC;EAE/B,MAAMkB,MAAM,GAAGnB,gBAAgB,CAAC,OAAO;IAACoB,SAAS,EAAE,CAAC;MAACF,KAAK,EAAEA,KAAK,CAACG;IAAK,CAAC;EAAC,CAAC,CAAC,EAAE,EAAE,CAAC;EAEhF,MAAMC,SAAS,GAAG1B,WAAW,CAAC,MAAM;IAClCsB,KAAK,CAACG,KAAK,GAAGnB,UAAU,CAAC,IAAI,EAAEG,MAAM,CAAC;EACxC,CAAC,EAAE,CAACa,KAAK,CAAC,CAAC;EAEX,MAAMK,UAAU,GAAG3B,WAAW,CAAC,MAAM;IACnCsB,KAAK,CAACG,KAAK,GAAGnB,UAAU,CAAC,CAAC,EAAEG,MAAM,CAAC;EACrC,CAAC,EAAE,CAACa,KAAK,CAAC,CAAC;EAEX,MAAMM,iBAAiB,GAAG5B,WAAW,CAAC,MAAM;IAC1C,IAAIgB,MAAM,EAAE;MACVjB,OAAO,CAAC8B,WAAW,CAAC9B,OAAO,CAAC+B,mBAAmB,CAACC,KAAK,CAAC,CAACC,KAAK,CAAC,MAAMC,SAAS,CAAC;IAC/E;IAEAhB,OAAO,GAAG,CAAC;EACb,CAAC,EAAE,CAACD,MAAM,EAAEC,OAAO,CAAC,CAAC;EAErB,oBACET,IAAA,CAACI,iBAAiB;IAChBG,QAAQ,EAAEA,QAAS;IACnBmB,OAAO,EAAE,CAAE;IACXjB,OAAO,EAAEW,iBAAkB;IAC3BF,SAAS,EAAEA,SAAU;IACrBC,UAAU,EAAEA,UAAW;IACvBQ,KAAK,EAAE,CAACf,QAAQ,EAAEG,MAAM,CAAE;IAC1BL,MAAM,EAAEA,MAAO;IAAA,GACXG,IAAI;IAAAP,QAAA,EAEPA;EAAQ,CACQ,CAAC;AAExB,CAAC","ignoreList":[]}
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
- onChange?.(event.nativeEvent.selectedSegmentIndex, event.nativeEvent.value);
17
- }, [onChange]);
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;AAIjC,OAAO,MAAML,gBAA2C,GAAGA,CAAC;EAC1DM,QAAQ,GAAG,KAAK;EAChBC,QAAQ;EACRC,aAAa;EACbC,MAAM;EACNC;AACqB,CAAC,KAAK;EAC3B,MAAMC,KAAK,GAAGT,WAAW,CAAC,CAAC;EAE3B,MAAMU,cAAc,GAAGT,WAAW,CAC/BU,KAAmE,IAAK;IACvEN,QAAQ,GAAGM,KAAK,CAACC,WAAW,CAACC,oBAAoB,EAAEF,KAAK,CAACC,WAAW,CAACE,KAAK,CAAC;EAC7E,CAAC,EACD,CAACT,QAAQ,CACX,CAAC;EAED,oBACEF,IAAA,CAACJ,sBAAsB;IACrBgB,UAAU,EAAEN,KAAK,CAACO,IAAI,GAAG,MAAM,GAAG,OAAQ;IAC1CC,OAAO,EAAE,CAACb,QAAS;IACnBC,QAAQ,EAAEK,cAAe;IACzBJ,aAAa,EAAEA,aAAc;IAC7BC,MAAM,EAAEA,MAAO;IACfW,SAAS,EAAET,KAAK,CAACU,WAAW,CAAC,iBAAiB,CAAE;IAChDX,MAAM,EAAEA;EAAO,CAChB,CAAC;AAEN,CAAC","ignoreList":[]}
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: onValueChange,
26
- steps: step,
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","steps"],"sourceRoot":"../../../../src","sources":["atoms/slider/slider.android.tsx"],"mappings":";;AAAA,SAAQA,MAAM,IAAIC,aAAa,QAAO,0BAA0B;AAChE,SAAQC,WAAW,QAAO,mBAAmB;AAE7C,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG/C,OAAO,MAAML,MAAuB,GAAGA,CAAC;EACtCM,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,GAAGV,WAAW,CAAC,CAAC;EAE3B,oBACEG,IAAA,CAACJ,aAAa;IACZY,MAAM,EAAE;MACNC,gBAAgB,EAAEF,KAAK,CAACG,KAAK,CAAC,qBAAqB,CAAC;MACpDC,UAAU,EAAEJ,KAAK,CAACG,KAAK,CAAC,qBAAqB;IAC/C,CAAE;IACFE,OAAO,EAAE,CAACX,QAAS;IACnBC,GAAG,EAAEA,GAAI;IACTC,GAAG,EAAEA,GAAI;IACTC,aAAa,EAAEA,aAAc;IAC7BS,KAAK,EAAER,IAAK;IACZC,KAAK,EAAEA;EAAM,CACd,CAAC;AAEN,CAAC","ignoreList":[]}
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 rowModifiers = useMemo(() => disabled || !onValueChange ? [fillMaxWidth()] : [fillMaxWidth(), clickable(() => onValueChange(!value))], [disabled, onValueChange, value]);
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;AAE7B,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA,EAAAC,IAAA,IAAAC,KAAA;AAG/C;AACA;AACA;AACA;AACA,OAAO,MAAMZ,MAAuB,GAAGA,CAAC;EAACa,QAAQ,GAAG,KAAK;EAAEC,KAAK;EAAEC,aAAa;EAAEC;AAAkB,CAAC,KAAK;EACvGR,YAAY,CAAC,QAAQ,CAAC;EACtB,MAAMS,KAAK,GAAGX,WAAW,CAAC,CAAC;EAC3B,MAAMY,YAAY,GAAGX,OAAO,CAC1B,MAAOM,QAAQ,IAAI,CAACE,aAAa,GAAG,CAACV,YAAY,CAAC,CAAC,CAAC,GAAG,CAACA,YAAY,CAAC,CAAC,EAAED,SAAS,CAAC,MAAMW,aAAa,CAAC,CAACC,KAAK,CAAC,CAAC,CAAE,EAChH,CAACH,QAAQ,EAAEE,aAAa,EAAEC,KAAK,CACjC,CAAC;EACD,MAAMG,OAAO,gBACXT,IAAA,CAACT,aAAa;IACZmB,MAAM,EAAE;MACNC,kBAAkB,EAAEJ,KAAK,CAACK,KAAK,CAAC,qBAAqB,CAAC;MACtDC,iBAAiB,EAAEN,KAAK,CAACK,KAAK,CAAC,eAAe,CAAC;MAC/CE,iBAAiB,EAAEP,KAAK,CAACK,KAAK,CAAC,qBAAqB;IACtD,CAAE;IACFG,OAAO,EAAE,CAACZ,QAAS;IACnBa,eAAe,EAAEX,aAAc;IAC/BC,KAAK,EAAEA;EAAM,CACd,CACF;EAED,IAAI,CAACF,KAAK,EAAE;IACV,OAAOK,OAAO;EAChB;EAEA,oBACEP,KAAA,CAACV,GAAG;IAACyB,qBAAqB,EAAC,cAAc;IAACC,SAAS,EAAEV,YAAa;IAACW,iBAAiB,EAAC,QAAQ;IAAAC,QAAA,gBAC3FpB,IAAA,CAACP,IAAI;MAACmB,KAAK,EAAEL,KAAK,CAACc,WAAW,CAAClB,QAAQ,GAAG,kBAAkB,GAAG,iBAAiB,CAAE;MAACmB,KAAK,EAAEf,KAAK,CAACgB,UAAU,CAACC,IAAK;MAAAJ,QAAA,EAC7GhB;IAAK,CACF,CAAC,EACNK,OAAO;EAAA,CACL,CAAC;AAEV,CAAC","ignoreList":[]}
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,5 @@
1
+ "use strict";
2
+
3
+ export { haptics } from "./haptics.js";
4
+ export { useHapticFeedback } from "./use-haptic-feedback.js";
5
+ //# sourceMappingURL=index.js.map
@@ -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":[]}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  export * from "./atoms/index.js";
4
+ export * from "./haptics/index.js";
4
5
  export * from "./molecules/index.js";
5
6
  export * from "./organisms/index.js";
6
7
  export * from "./templates/index.js";
@@ -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";
@@ -87,7 +87,7 @@ const ContextMenuRoot = ({
87
87
  pageX,
88
88
  pageY
89
89
  } = event.nativeEvent;
90
- Haptics.performAndroidHapticsAsync(Haptics.AndroidHaptics.Long_Press).catch(() => undefined);
90
+ haptics.longPress();
91
91
  // The touch in the trigger's own coordinates: `locationX/Y` are relative to whichever child was touched.
92
92
  trigger.current?.measure((_x, _y, _width, _height, left, top) => {
93
93
  setAnchor({
@@ -1 +1 @@
1
- {"version":3,"names":["Text","ComposeText","DropdownMenu","DropdownMenuItem","Host","useSx","useAppTheme","Haptics","useCallback","useMemo","useRef","useState","Pressable","StyleSheet","View","Icon","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","performAndroidHapticsAsync","AndroidHaptics","Long_Press","catch","undefined","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":[]}
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":[]}
@@ -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?: boolean;
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,2 @@
1
+ export { type Haptic, type HapticOptions, haptics } from './haptics';
2
+ export { type HapticResolver, useHapticFeedback } from './use-haptic-feedback';
@@ -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;
@@ -1,4 +1,5 @@
1
1
  export * from './atoms';
2
+ export * from './haptics';
2
3
  export * from './molecules';
3
4
  export * from './organisms';
4
5
  export * from './templates';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.5.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.5.0",
63
- "@hzblj/kara-sx": "^0.5.0",
64
- "@hzblj/kara-theme": "^0.5.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",
@@ -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
- }, [scale])
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 (haptic) {
35
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => undefined)
41
+ if (feedback) {
42
+ haptics.play(feedback)
36
43
  }
37
44
 
38
45
  onPress?.()
39
- }, [haptic, onPress])
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?: boolean
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
- onChange?.(event.nativeEvent.selectedSegmentIndex, event.nativeEvent.value)
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={onValueChange}
28
- steps={step}
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(() => onValueChange(!value))]),
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,2 @@
1
+ export {type Haptic, type HapticOptions, haptics} from './haptics'
2
+ export {type HapticResolver, useHapticFeedback} from './use-haptic-feedback'
@@ -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,4 +1,5 @@
1
1
  export * from './atoms'
2
+ export * from './haptics'
2
3
  export * from './molecules'
3
4
  export * from './organisms'
4
5
  export * from './templates'
@@ -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'
@@ -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
- Haptics.performAndroidHapticsAsync(Haptics.AndroidHaptics.Long_Press).catch(() => undefined)
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})