@oxyhq/bloom 0.28.0 → 0.29.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.
@@ -19,19 +19,26 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
19
19
  */
20
20
  const IS_WEB_TOUCH_DEVICE = _reactNative.Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
21
21
  const SCALE_SUPPORTED = _reactNative.Platform.OS !== 'web' || IS_WEB_TOUCH_DEVICE;
22
+
23
+ /**
24
+ * A single animated `Pressable`, built once at module scope so the animated
25
+ * node identity is stable across renders.
26
+ */
27
+ const AnimatedPressable = _reactNativeReanimated.default.createAnimatedComponent(_reactNative.Pressable);
22
28
  /**
23
- * A drop-in {@link Pressable} whose content springs to a smaller scale on
24
- * press-in and back on release. The outer pressable stays full size (so the
25
- * touch target never shrinks) while an inner wrapper scales.
29
+ * A drop-in {@link Pressable} that springs to a smaller scale on press-in and
30
+ * back on release. The scale is applied to the pressable node itself, so a
31
+ * consumer's `className` / layout styles (e.g. `flex-row items-center`) land on
32
+ * the same node that animates.
26
33
  *
34
+ * The touch target scales with the content — acceptable at the default ~0.98
35
+ * (the earlier two-node touch-target-preservation was dropped for simplicity).
27
36
  * Honours the OS "reduce motion" setting and disables the effect on non-touch
28
37
  * web pointers.
29
38
  */
30
39
  const PressableScale = exports.PressableScale = /*#__PURE__*/(0, _react.forwardRef)(function PressableScale({
31
40
  targetScale = 0.98,
32
41
  style,
33
- contentContainerStyle,
34
- children,
35
42
  onPressIn,
36
43
  onPressOut,
37
44
  ...rest
@@ -44,7 +51,7 @@ const PressableScale = exports.PressableScale = /*#__PURE__*/(0, _react.forwardR
44
51
  scale: scale.value
45
52
  }]
46
53
  }));
47
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
54
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedPressable, {
48
55
  ref: ref,
49
56
  accessibilityRole: "button",
50
57
  onPressIn: e => {
@@ -63,12 +70,8 @@ const PressableScale = exports.PressableScale = /*#__PURE__*/(0, _react.forwardR
63
70
  duration: DURATION
64
71
  });
65
72
  },
66
- style: style,
67
- ...rest,
68
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
69
- style: [animate ? animatedStyle : null, contentContainerStyle],
70
- children: children
71
- })
73
+ style: [animate ? animatedStyle : null, style],
74
+ ...rest
72
75
  });
73
76
  });
74
77
  PressableScale.displayName = 'PressableScale';
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_reactNativeReanimated","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","DURATION","IS_WEB_TOUCH_DEVICE","Platform","OS","window","matchMedia","matches","SCALE_SUPPORTED","PressableScale","exports","forwardRef","targetScale","style","contentContainerStyle","children","onPressIn","onPressOut","rest","ref","reducedMotion","useReducedMotion","animate","scale","useSharedValue","animatedStyle","useAnimatedStyle","transform","value","jsx","Pressable","accessibilityRole","cancelAnimation","withTiming","duration","View","displayName"],"sourceRoot":"../../../src","sources":["pressable-scale/PressableScale.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_reactNativeReanimated","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","DURATION","IS_WEB_TOUCH_DEVICE","Platform","OS","window","matchMedia","matches","SCALE_SUPPORTED","AnimatedPressable","Animated","createAnimatedComponent","Pressable","PressableScale","exports","forwardRef","targetScale","style","onPressIn","onPressOut","rest","ref","reducedMotion","useReducedMotion","animate","scale","useSharedValue","animatedStyle","useAnimatedStyle","transform","value","jsx","accessibilityRole","cancelAnimation","withTiming","duration","displayName"],"sourceRoot":"../../../src","sources":["pressable-scale/PressableScale.tsx"],"mappings":"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","ignoreList":[]}
@@ -10,49 +10,54 @@ var _index = require("../theme/index.js");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
12
  /**
13
- * True on touch-capable web browsers (coarse pointer). A hover wash only makes
14
- * sense with a hovering pointer, so it is suppressed on touch web to match
15
- * native (which has no hover at all).
13
+ * True on touch-capable web browsers (coarse pointer). The CSS `group-hover`
14
+ * wash only makes sense with a hovering pointer, so it is suppressed on touch
15
+ * web to match native (which has no hover at all).
16
16
  */const IS_WEB_TOUCH_DEVICE = _reactNative.Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
17
-
18
- /**
19
- * Web-only style that (a) cross-fades the overlay opacity and (b) promotes it to
20
- * its own GPU layer so Safari doesn't nudge the layout by a subpixel when the
21
- * overlay composites on hover. Cast because RN's `ViewStyle` has no `willChange`
22
- * / `transition*` keys; react-native-web forwards them to the DOM node.
23
- */
24
- const WEB_TRANSITION = {
25
- transitionProperty: 'opacity',
26
- transitionDuration: '0.1s',
27
- transitionTimingFunction: 'ease',
28
- willChange: 'opacity'
29
- };
30
17
  /**
31
- * An absolutely-positioned, non-interactive contrast wash that fades in while
32
- * its parent is hovered. Ported from Bluesky's `SubtleHover`. Pairs naturally
33
- * with {@link useInteractionState} — pass its `state` as `hover`.
18
+ * An absolutely-positioned, non-interactive contrast wash. Two modes:
34
19
  *
35
- * Renders nothing on touch-web (no hovering pointer) and on native unless
36
- * `native` is set. Color comes from the theme's `contrast50` token so the wash
37
- * tracks the active preset and light/dark mode.
20
+ * - **CSS `group-hover`** (`active` omitted): the wash fades in on hover purely
21
+ * through NativeWind classes no React state. The parent must carry
22
+ * `className="group"`. Web-only (native and touch-web have no hover).
23
+ * - **JS `active`**: the wash opacity follows the boolean, so several elements
24
+ * can light up together (e.g. every post of a thread). Web always; native
25
+ * only when `native` is set.
26
+ *
27
+ * The wash color comes from the theme's `contrast50` token so it tracks the
28
+ * active preset and light/dark mode. The web cross-fade is a NativeWind
29
+ * `transition-opacity` class — no manual `willChange` / web-style casts.
38
30
  */
39
31
  function SubtleHover({
40
- hover,
41
- opacity,
32
+ active,
42
33
  style,
43
34
  native = false
44
35
  }) {
45
36
  const theme = (0, _index.useTheme)();
46
37
  const isWeb = _reactNative.Platform.OS === 'web';
47
- if (isWeb ? IS_WEB_TOUCH_DEVICE : !native) {
48
- return null;
38
+
39
+ // Full literal class strings (never concatenated fragments) so NativeWind's
40
+ // compiler statically extracts every variant.
41
+ if (active === undefined) {
42
+ // CSS group-hover: a web pointer affordance. Native has no hover, and
43
+ // neither does touch web — render nothing there.
44
+ if (!isWeb || IS_WEB_TOUCH_DEVICE) return null;
45
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
46
+ className: 'opacity-0 group-hover:opacity-50 dark:group-hover:opacity-40 transition-opacity duration-150',
47
+ style: [styles.overlay, {
48
+ backgroundColor: theme.colors.contrast50
49
+ }, style]
50
+ });
49
51
  }
50
- const targetOpacity = opacity ?? (theme.isDark ? 0.4 : 0.5);
52
+
53
+ // JS-driven coordinated mode: web always; native only when opted in.
54
+ if (!isWeb && !native) return null;
51
55
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
56
+ className: 'transition-opacity duration-150',
52
57
  style: [styles.overlay, {
53
58
  backgroundColor: theme.colors.contrast50,
54
- opacity: hover ? targetOpacity : 0
55
- }, isWeb ? WEB_TRANSITION : null, style]
59
+ opacity: active ? theme.isDark ? 0.4 : 0.5 : 0
60
+ }, style]
56
61
  });
57
62
  }
58
63
  SubtleHover.displayName = 'SubtleHover';
@@ -1 +1 @@
1
- {"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_index","_jsxRuntime","e","__esModule","default","IS_WEB_TOUCH_DEVICE","Platform","OS","window","matchMedia","matches","WEB_TRANSITION","transitionProperty","transitionDuration","transitionTimingFunction","willChange","SubtleHover","hover","opacity","style","native","theme","useTheme","isWeb","targetOpacity","isDark","jsx","View","styles","overlay","backgroundColor","colors","contrast50","displayName","StyleSheet","create","position","top","left","right","bottom","pointerEvents"],"sourceRoot":"../../../src","sources":["subtle-hover/SubtleHover.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AAEA,IAAAE,MAAA,GAAAF,OAAA;AAAoC,IAAAG,WAAA,GAAAH,OAAA;AAAA,SAAAD,uBAAAK,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAEpC;AACA;AACA;AACA;AACA,GACA,MAAMG,mBAAmB,GACvBC,qBAAQ,CAACC,EAAE,KAAK,KAAK,IACrB,OAAOC,MAAM,KAAK,WAAW,IAC7B,OAAOA,MAAM,CAACC,UAAU,KAAK,UAAU,IACvCD,MAAM,CAACC,UAAU,CAAC,mBAAmB,CAAC,CAACC,OAAO;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,cAAc,GAAG;EACrBC,kBAAkB,EAAE,SAAS;EAC7BC,kBAAkB,EAAE,MAAM;EAC1BC,wBAAwB,EAAE,MAAM;EAChCC,UAAU,EAAE;AACd,CAAc;AAoBd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,WAAWA,CAAC;EAAEC,KAAK;EAAEC,OAAO;EAAEC,KAAK;EAAEC,MAAM,GAAG;AAAwB,CAAC,EAAE;EACvF,MAAMC,KAAK,GAAG,IAAAC,eAAQ,EAAC,CAAC;EAExB,MAAMC,KAAK,GAAGjB,qBAAQ,CAACC,EAAE,KAAK,KAAK;EACnC,IAAIgB,KAAK,GAAGlB,mBAAmB,GAAG,CAACe,MAAM,EAAE;IACzC,OAAO,IAAI;EACb;EAEA,MAAMI,aAAa,GAAGN,OAAO,KAAKG,KAAK,CAACI,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC;EAE3D,oBACE,IAAAxB,WAAA,CAAAyB,GAAA,EAAC3B,YAAA,CAAA4B,IAAI;IACHR,KAAK,EAAE,CACLS,MAAM,CAACC,OAAO,EACd;MAAEC,eAAe,EAAET,KAAK,CAACU,MAAM,CAACC,UAAU;MAAEd,OAAO,EAAED,KAAK,GAAGO,aAAa,GAAG;IAAE,CAAC,EAChFD,KAAK,GAAGZ,cAAc,GAAG,IAAI,EAC7BQ,KAAK;EACL,CACH,CAAC;AAEN;AAEAH,WAAW,CAACiB,WAAW,GAAG,aAAa;AAEvC,MAAML,MAAM,GAAGM,uBAAU,CAACC,MAAM,CAAC;EAC/BN,OAAO,EAAE;IACPO,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE,CAAC;IACTC,aAAa,EAAE;EACjB;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_react","_interopRequireDefault","require","_reactNative","_index","_jsxRuntime","e","__esModule","default","IS_WEB_TOUCH_DEVICE","Platform","OS","window","matchMedia","matches","SubtleHover","active","style","native","theme","useTheme","isWeb","undefined","jsx","View","className","styles","overlay","backgroundColor","colors","contrast50","opacity","isDark","displayName","StyleSheet","create","position","top","left","right","bottom","pointerEvents"],"sourceRoot":"../../../src","sources":["subtle-hover/SubtleHover.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AAEA,IAAAE,MAAA,GAAAF,OAAA;AAAoC,IAAAG,WAAA,GAAAH,OAAA;AAAA,SAAAD,uBAAAK,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAEpC;AACA;AACA;AACA;AACA,GACA,MAAMG,mBAAmB,GACvBC,qBAAQ,CAACC,EAAE,KAAK,KAAK,IACrB,OAAOC,MAAM,KAAK,WAAW,IAC7B,OAAOA,MAAM,CAACC,UAAU,KAAK,UAAU,IACvCD,MAAM,CAACC,UAAU,CAAC,mBAAmB,CAAC,CAACC,OAAO;AAoBhD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,WAAWA,CAAC;EAAEC,MAAM;EAAEC,KAAK;EAAEC,MAAM,GAAG;AAAwB,CAAC,EAAE;EAC/E,MAAMC,KAAK,GAAG,IAAAC,eAAQ,EAAC,CAAC;EACxB,MAAMC,KAAK,GAAGX,qBAAQ,CAACC,EAAE,KAAK,KAAK;;EAEnC;EACA;EACA,IAAIK,MAAM,KAAKM,SAAS,EAAE;IACxB;IACA;IACA,IAAI,CAACD,KAAK,IAAIZ,mBAAmB,EAAE,OAAO,IAAI;IAC9C,oBACE,IAAAJ,WAAA,CAAAkB,GAAA,EAACpB,YAAA,CAAAqB,IAAI;MAEDC,SAAS,EACP,8FAA8F;MAElGR,KAAK,EAAE,CAACS,MAAM,CAACC,OAAO,EAAE;QAAEC,eAAe,EAAET,KAAK,CAACU,MAAM,CAACC;MAAW,CAAC,EAAEb,KAAK;IAAE,CAC9E,CAAC;EAEN;;EAEA;EACA,IAAI,CAACI,KAAK,IAAI,CAACH,MAAM,EAAE,OAAO,IAAI;EAClC,oBACE,IAAAb,WAAA,CAAAkB,GAAA,EAACpB,YAAA,CAAAqB,IAAI;IACIC,SAAS,EAAE,iCAAiC;IACnDR,KAAK,EAAE,CACLS,MAAM,CAACC,OAAO,EACd;MACEC,eAAe,EAAET,KAAK,CAACU,MAAM,CAACC,UAAU;MACxCC,OAAO,EAAEf,MAAM,GAAIG,KAAK,CAACa,MAAM,GAAG,GAAG,GAAG,GAAG,GAAI;IACjD,CAAC,EACDf,KAAK;EACL,CACH,CAAC;AAEN;AAEAF,WAAW,CAACkB,WAAW,GAAG,aAAa;AAEvC,MAAMP,MAAM,GAAGQ,uBAAU,CAACC,MAAM,CAAC;EAC/BR,OAAO,EAAE;IACPS,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE,CAAC;IACTC,aAAa,EAAE;EACjB;AACF,CAAC,CAAC","ignoreList":[]}
@@ -16,19 +16,26 @@ const DURATION = 100;
16
16
  */
17
17
  const IS_WEB_TOUCH_DEVICE = Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
18
18
  const SCALE_SUPPORTED = Platform.OS !== 'web' || IS_WEB_TOUCH_DEVICE;
19
+
20
+ /**
21
+ * A single animated `Pressable`, built once at module scope so the animated
22
+ * node identity is stable across renders.
23
+ */
24
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
19
25
  /**
20
- * A drop-in {@link Pressable} whose content springs to a smaller scale on
21
- * press-in and back on release. The outer pressable stays full size (so the
22
- * touch target never shrinks) while an inner wrapper scales.
26
+ * A drop-in {@link Pressable} that springs to a smaller scale on press-in and
27
+ * back on release. The scale is applied to the pressable node itself, so a
28
+ * consumer's `className` / layout styles (e.g. `flex-row items-center`) land on
29
+ * the same node that animates.
23
30
  *
31
+ * The touch target scales with the content — acceptable at the default ~0.98
32
+ * (the earlier two-node touch-target-preservation was dropped for simplicity).
24
33
  * Honours the OS "reduce motion" setting and disables the effect on non-touch
25
34
  * web pointers.
26
35
  */
27
36
  export const PressableScale = /*#__PURE__*/forwardRef(function PressableScale({
28
37
  targetScale = 0.98,
29
38
  style,
30
- contentContainerStyle,
31
- children,
32
39
  onPressIn,
33
40
  onPressOut,
34
41
  ...rest
@@ -41,7 +48,7 @@ export const PressableScale = /*#__PURE__*/forwardRef(function PressableScale({
41
48
  scale: scale.value
42
49
  }]
43
50
  }));
44
- return /*#__PURE__*/_jsx(Pressable, {
51
+ return /*#__PURE__*/_jsx(AnimatedPressable, {
45
52
  ref: ref,
46
53
  accessibilityRole: "button",
47
54
  onPressIn: e => {
@@ -60,12 +67,8 @@ export const PressableScale = /*#__PURE__*/forwardRef(function PressableScale({
60
67
  duration: DURATION
61
68
  });
62
69
  },
63
- style: style,
64
- ...rest,
65
- children: /*#__PURE__*/_jsx(Animated.View, {
66
- style: [animate ? animatedStyle : null, contentContainerStyle],
67
- children: children
68
- })
70
+ style: [animate ? animatedStyle : null, style],
71
+ ...rest
69
72
  });
70
73
  });
71
74
  PressableScale.displayName = 'PressableScale';
@@ -1 +1 @@
1
- {"version":3,"names":["React","forwardRef","Platform","Pressable","Animated","cancelAnimation","useAnimatedStyle","useReducedMotion","useSharedValue","withTiming","jsx","_jsx","DURATION","IS_WEB_TOUCH_DEVICE","OS","window","matchMedia","matches","SCALE_SUPPORTED","PressableScale","targetScale","style","contentContainerStyle","children","onPressIn","onPressOut","rest","ref","reducedMotion","animate","scale","animatedStyle","transform","value","accessibilityRole","e","duration","View","displayName"],"sourceRoot":"../../../src","sources":["pressable-scale/PressableScale.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,UAAU,QAAQ,OAAO;AACzC,SACEC,QAAQ,EACRC,SAAS,QAKJ,cAAc;AACrB,OAAOC,QAAQ,IACbC,eAAe,EACfC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,EACdC,UAAU,QACL,yBAAyB;;AAEhC;AAAA,SAAAC,GAAA,IAAAC,IAAA;AACA,MAAMC,QAAQ,GAAG,GAAG;;AAEpB;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,mBAAmB,GACvBX,QAAQ,CAACY,EAAE,KAAK,KAAK,IACrB,OAAOC,MAAM,KAAK,WAAW,IAC7B,OAAOA,MAAM,CAACC,UAAU,KAAK,UAAU,IACvCD,MAAM,CAACC,UAAU,CAAC,mBAAmB,CAAC,CAACC,OAAO;AAEhD,MAAMC,eAAe,GAAGhB,QAAQ,CAACY,EAAE,KAAK,KAAK,IAAID,mBAAmB;AAapE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMM,cAAc,gBAAGlB,UAAU,CACtC,SAASkB,cAAcA,CACrB;EACEC,WAAW,GAAG,IAAI;EAClBC,KAAK;EACLC,qBAAqB;EACrBC,QAAQ;EACRC,SAAS;EACTC,UAAU;EACV,GAAGC;AACL,CAAC,EACDC,GAAG,EACH;EACA,MAAMC,aAAa,GAAGrB,gBAAgB,CAAC,CAAC;EACxC,MAAMsB,OAAO,GAAGX,eAAe,IAAI,CAACU,aAAa;EAEjD,MAAME,KAAK,GAAGtB,cAAc,CAAC,CAAC,CAAC;EAE/B,MAAMuB,aAAa,GAAGzB,gBAAgB,CAAC,OAAO;IAC5C0B,SAAS,EAAE,CAAC;MAAEF,KAAK,EAAEA,KAAK,CAACG;IAAM,CAAC;EACpC,CAAC,CAAC,CAAC;EAEH,oBACEtB,IAAA,CAACR,SAAS;IACRwB,GAAG,EAAEA,GAAI;IACTO,iBAAiB,EAAC,QAAQ;IAC1BV,SAAS,EAAGW,CAAC,IAAK;MAChBX,SAAS,GAAGW,CAAC,CAAC;MACd,IAAI,CAACN,OAAO,EAAE;MACdxB,eAAe,CAACyB,KAAK,CAAC;MACtBA,KAAK,CAACG,KAAK,GAAGxB,UAAU,CAACW,WAAW,EAAE;QAAEgB,QAAQ,EAAExB;MAAS,CAAC,CAAC;IAC/D,CAAE;IACFa,UAAU,EAAGU,CAAC,IAAK;MACjBV,UAAU,GAAGU,CAAC,CAAC;MACf,IAAI,CAACN,OAAO,EAAE;MACdxB,eAAe,CAACyB,KAAK,CAAC;MACtBA,KAAK,CAACG,KAAK,GAAGxB,UAAU,CAAC,CAAC,EAAE;QAAE2B,QAAQ,EAAExB;MAAS,CAAC,CAAC;IACrD,CAAE;IACFS,KAAK,EAAEA,KAAM;IAAA,GACTK,IAAI;IAAAH,QAAA,eAERZ,IAAA,CAACP,QAAQ,CAACiC,IAAI;MAAChB,KAAK,EAAE,CAACQ,OAAO,GAAGE,aAAa,GAAG,IAAI,EAAET,qBAAqB,CAAE;MAAAC,QAAA,EAC3EA;IAAQ,CACI;EAAC,CACP,CAAC;AAEhB,CACF,CAAC;AAEDJ,cAAc,CAACmB,WAAW,GAAG,gBAAgB","ignoreList":[]}
1
+ {"version":3,"names":["React","forwardRef","Platform","Pressable","Animated","cancelAnimation","useAnimatedStyle","useReducedMotion","useSharedValue","withTiming","jsx","_jsx","DURATION","IS_WEB_TOUCH_DEVICE","OS","window","matchMedia","matches","SCALE_SUPPORTED","AnimatedPressable","createAnimatedComponent","PressableScale","targetScale","style","onPressIn","onPressOut","rest","ref","reducedMotion","animate","scale","animatedStyle","transform","value","accessibilityRole","e","duration","displayName"],"sourceRoot":"../../../src","sources":["pressable-scale/PressableScale.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,UAAU,QAAQ,OAAO;AACzC,SACEC,QAAQ,EACRC,SAAS,QAKJ,cAAc;AACrB,OAAOC,QAAQ,IACbC,eAAe,EACfC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,EACdC,UAAU,QACL,yBAAyB;;AAEhC;AAAA,SAAAC,GAAA,IAAAC,IAAA;AACA,MAAMC,QAAQ,GAAG,GAAG;;AAEpB;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,mBAAmB,GACvBX,QAAQ,CAACY,EAAE,KAAK,KAAK,IACrB,OAAOC,MAAM,KAAK,WAAW,IAC7B,OAAOA,MAAM,CAACC,UAAU,KAAK,UAAU,IACvCD,MAAM,CAACC,UAAU,CAAC,mBAAmB,CAAC,CAACC,OAAO;AAEhD,MAAMC,eAAe,GAAGhB,QAAQ,CAACY,EAAE,KAAK,KAAK,IAAID,mBAAmB;;AAEpE;AACA;AACA;AACA;AACA,MAAMM,iBAAiB,GAAGf,QAAQ,CAACgB,uBAAuB,CAACjB,SAAS,CAAC;AASrE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMkB,cAAc,gBAAGpB,UAAU,CACtC,SAASoB,cAAcA,CAAC;EAAEC,WAAW,GAAG,IAAI;EAAEC,KAAK;EAAEC,SAAS;EAAEC,UAAU;EAAE,GAAGC;AAAK,CAAC,EAAEC,GAAG,EAAE;EAC1F,MAAMC,aAAa,GAAGrB,gBAAgB,CAAC,CAAC;EACxC,MAAMsB,OAAO,GAAGX,eAAe,IAAI,CAACU,aAAa;EAEjD,MAAME,KAAK,GAAGtB,cAAc,CAAC,CAAC,CAAC;EAE/B,MAAMuB,aAAa,GAAGzB,gBAAgB,CAAC,OAAO;IAC5C0B,SAAS,EAAE,CAAC;MAAEF,KAAK,EAAEA,KAAK,CAACG;IAAM,CAAC;EACpC,CAAC,CAAC,CAAC;EAEH,oBACEtB,IAAA,CAACQ,iBAAiB;IAChBQ,GAAG,EAAEA,GAAI;IACTO,iBAAiB,EAAC,QAAQ;IAC1BV,SAAS,EAAGW,CAAC,IAAK;MAChBX,SAAS,GAAGW,CAAC,CAAC;MACd,IAAI,CAACN,OAAO,EAAE;MACdxB,eAAe,CAACyB,KAAK,CAAC;MACtBA,KAAK,CAACG,KAAK,GAAGxB,UAAU,CAACa,WAAW,EAAE;QAAEc,QAAQ,EAAExB;MAAS,CAAC,CAAC;IAC/D,CAAE;IACFa,UAAU,EAAGU,CAAC,IAAK;MACjBV,UAAU,GAAGU,CAAC,CAAC;MACf,IAAI,CAACN,OAAO,EAAE;MACdxB,eAAe,CAACyB,KAAK,CAAC;MACtBA,KAAK,CAACG,KAAK,GAAGxB,UAAU,CAAC,CAAC,EAAE;QAAE2B,QAAQ,EAAExB;MAAS,CAAC,CAAC;IACrD,CAAE;IACFW,KAAK,EAAE,CAACM,OAAO,GAAGE,aAAa,GAAG,IAAI,EAAER,KAAK,CAAE;IAAA,GAC3CG;EAAI,CACT,CAAC;AAEN,CACF,CAAC;AAEDL,cAAc,CAACgB,WAAW,GAAG,gBAAgB","ignoreList":[]}
@@ -5,51 +5,56 @@ import { Platform, StyleSheet, View } from 'react-native';
5
5
  import { useTheme } from "../theme/index.js";
6
6
 
7
7
  /**
8
- * True on touch-capable web browsers (coarse pointer). A hover wash only makes
9
- * sense with a hovering pointer, so it is suppressed on touch web to match
10
- * native (which has no hover at all).
8
+ * True on touch-capable web browsers (coarse pointer). The CSS `group-hover`
9
+ * wash only makes sense with a hovering pointer, so it is suppressed on touch
10
+ * web to match native (which has no hover at all).
11
11
  */
12
12
  import { jsx as _jsx } from "react/jsx-runtime";
13
13
  const IS_WEB_TOUCH_DEVICE = Platform.OS === 'web' && typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(pointer: coarse)').matches;
14
-
15
- /**
16
- * Web-only style that (a) cross-fades the overlay opacity and (b) promotes it to
17
- * its own GPU layer so Safari doesn't nudge the layout by a subpixel when the
18
- * overlay composites on hover. Cast because RN's `ViewStyle` has no `willChange`
19
- * / `transition*` keys; react-native-web forwards them to the DOM node.
20
- */
21
- const WEB_TRANSITION = {
22
- transitionProperty: 'opacity',
23
- transitionDuration: '0.1s',
24
- transitionTimingFunction: 'ease',
25
- willChange: 'opacity'
26
- };
27
14
  /**
28
- * An absolutely-positioned, non-interactive contrast wash that fades in while
29
- * its parent is hovered. Ported from Bluesky's `SubtleHover`. Pairs naturally
30
- * with {@link useInteractionState} — pass its `state` as `hover`.
15
+ * An absolutely-positioned, non-interactive contrast wash. Two modes:
31
16
  *
32
- * Renders nothing on touch-web (no hovering pointer) and on native unless
33
- * `native` is set. Color comes from the theme's `contrast50` token so the wash
34
- * tracks the active preset and light/dark mode.
17
+ * - **CSS `group-hover`** (`active` omitted): the wash fades in on hover purely
18
+ * through NativeWind classes no React state. The parent must carry
19
+ * `className="group"`. Web-only (native and touch-web have no hover).
20
+ * - **JS `active`**: the wash opacity follows the boolean, so several elements
21
+ * can light up together (e.g. every post of a thread). Web always; native
22
+ * only when `native` is set.
23
+ *
24
+ * The wash color comes from the theme's `contrast50` token so it tracks the
25
+ * active preset and light/dark mode. The web cross-fade is a NativeWind
26
+ * `transition-opacity` class — no manual `willChange` / web-style casts.
35
27
  */
36
28
  export function SubtleHover({
37
- hover,
38
- opacity,
29
+ active,
39
30
  style,
40
31
  native = false
41
32
  }) {
42
33
  const theme = useTheme();
43
34
  const isWeb = Platform.OS === 'web';
44
- if (isWeb ? IS_WEB_TOUCH_DEVICE : !native) {
45
- return null;
35
+
36
+ // Full literal class strings (never concatenated fragments) so NativeWind's
37
+ // compiler statically extracts every variant.
38
+ if (active === undefined) {
39
+ // CSS group-hover: a web pointer affordance. Native has no hover, and
40
+ // neither does touch web — render nothing there.
41
+ if (!isWeb || IS_WEB_TOUCH_DEVICE) return null;
42
+ return /*#__PURE__*/_jsx(View, {
43
+ className: 'opacity-0 group-hover:opacity-50 dark:group-hover:opacity-40 transition-opacity duration-150',
44
+ style: [styles.overlay, {
45
+ backgroundColor: theme.colors.contrast50
46
+ }, style]
47
+ });
46
48
  }
47
- const targetOpacity = opacity ?? (theme.isDark ? 0.4 : 0.5);
49
+
50
+ // JS-driven coordinated mode: web always; native only when opted in.
51
+ if (!isWeb && !native) return null;
48
52
  return /*#__PURE__*/_jsx(View, {
53
+ className: 'transition-opacity duration-150',
49
54
  style: [styles.overlay, {
50
55
  backgroundColor: theme.colors.contrast50,
51
- opacity: hover ? targetOpacity : 0
52
- }, isWeb ? WEB_TRANSITION : null, style]
56
+ opacity: active ? theme.isDark ? 0.4 : 0.5 : 0
57
+ }, style]
53
58
  });
54
59
  }
55
60
  SubtleHover.displayName = 'SubtleHover';
@@ -1 +1 @@
1
- {"version":3,"names":["React","Platform","StyleSheet","View","useTheme","jsx","_jsx","IS_WEB_TOUCH_DEVICE","OS","window","matchMedia","matches","WEB_TRANSITION","transitionProperty","transitionDuration","transitionTimingFunction","willChange","SubtleHover","hover","opacity","style","native","theme","isWeb","targetOpacity","isDark","styles","overlay","backgroundColor","colors","contrast50","displayName","create","position","top","left","right","bottom","pointerEvents"],"sourceRoot":"../../../src","sources":["subtle-hover/SubtleHover.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,QAAQ,EAAEC,UAAU,EAAEC,IAAI,QAAwC,cAAc;AAEzF,SAASC,QAAQ,QAAQ,mBAAU;;AAEnC;AACA;AACA;AACA;AACA;AAJA,SAAAC,GAAA,IAAAC,IAAA;AAKA,MAAMC,mBAAmB,GACvBN,QAAQ,CAACO,EAAE,KAAK,KAAK,IACrB,OAAOC,MAAM,KAAK,WAAW,IAC7B,OAAOA,MAAM,CAACC,UAAU,KAAK,UAAU,IACvCD,MAAM,CAACC,UAAU,CAAC,mBAAmB,CAAC,CAACC,OAAO;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,cAAc,GAAG;EACrBC,kBAAkB,EAAE,SAAS;EAC7BC,kBAAkB,EAAE,MAAM;EAC1BC,wBAAwB,EAAE,MAAM;EAChCC,UAAU,EAAE;AACd,CAAc;AAoBd;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,WAAWA,CAAC;EAAEC,KAAK;EAAEC,OAAO;EAAEC,KAAK;EAAEC,MAAM,GAAG;AAAwB,CAAC,EAAE;EACvF,MAAMC,KAAK,GAAGlB,QAAQ,CAAC,CAAC;EAExB,MAAMmB,KAAK,GAAGtB,QAAQ,CAACO,EAAE,KAAK,KAAK;EACnC,IAAIe,KAAK,GAAGhB,mBAAmB,GAAG,CAACc,MAAM,EAAE;IACzC,OAAO,IAAI;EACb;EAEA,MAAMG,aAAa,GAAGL,OAAO,KAAKG,KAAK,CAACG,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC;EAE3D,oBACEnB,IAAA,CAACH,IAAI;IACHiB,KAAK,EAAE,CACLM,MAAM,CAACC,OAAO,EACd;MAAEC,eAAe,EAAEN,KAAK,CAACO,MAAM,CAACC,UAAU;MAAEX,OAAO,EAAED,KAAK,GAAGM,aAAa,GAAG;IAAE,CAAC,EAChFD,KAAK,GAAGX,cAAc,GAAG,IAAI,EAC7BQ,KAAK;EACL,CACH,CAAC;AAEN;AAEAH,WAAW,CAACc,WAAW,GAAG,aAAa;AAEvC,MAAML,MAAM,GAAGxB,UAAU,CAAC8B,MAAM,CAAC;EAC/BL,OAAO,EAAE;IACPM,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE,CAAC;IACTC,aAAa,EAAE;EACjB;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["React","Platform","StyleSheet","View","useTheme","jsx","_jsx","IS_WEB_TOUCH_DEVICE","OS","window","matchMedia","matches","SubtleHover","active","style","native","theme","isWeb","undefined","className","styles","overlay","backgroundColor","colors","contrast50","opacity","isDark","displayName","create","position","top","left","right","bottom","pointerEvents"],"sourceRoot":"../../../src","sources":["subtle-hover/SubtleHover.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;AACzB,SAASC,QAAQ,EAAEC,UAAU,EAAEC,IAAI,QAAwC,cAAc;AAEzF,SAASC,QAAQ,QAAQ,mBAAU;;AAEnC;AACA;AACA;AACA;AACA;AAJA,SAAAC,GAAA,IAAAC,IAAA;AAKA,MAAMC,mBAAmB,GACvBN,QAAQ,CAACO,EAAE,KAAK,KAAK,IACrB,OAAOC,MAAM,KAAK,WAAW,IAC7B,OAAOA,MAAM,CAACC,UAAU,KAAK,UAAU,IACvCD,MAAM,CAACC,UAAU,CAAC,mBAAmB,CAAC,CAACC,OAAO;AAoBhD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,WAAWA,CAAC;EAAEC,MAAM;EAAEC,KAAK;EAAEC,MAAM,GAAG;AAAwB,CAAC,EAAE;EAC/E,MAAMC,KAAK,GAAGZ,QAAQ,CAAC,CAAC;EACxB,MAAMa,KAAK,GAAGhB,QAAQ,CAACO,EAAE,KAAK,KAAK;;EAEnC;EACA;EACA,IAAIK,MAAM,KAAKK,SAAS,EAAE;IACxB;IACA;IACA,IAAI,CAACD,KAAK,IAAIV,mBAAmB,EAAE,OAAO,IAAI;IAC9C,oBACED,IAAA,CAACH,IAAI;MAEDgB,SAAS,EACP,8FAA8F;MAElGL,KAAK,EAAE,CAACM,MAAM,CAACC,OAAO,EAAE;QAAEC,eAAe,EAAEN,KAAK,CAACO,MAAM,CAACC;MAAW,CAAC,EAAEV,KAAK;IAAE,CAC9E,CAAC;EAEN;;EAEA;EACA,IAAI,CAACG,KAAK,IAAI,CAACF,MAAM,EAAE,OAAO,IAAI;EAClC,oBACET,IAAA,CAACH,IAAI;IACIgB,SAAS,EAAE,iCAAiC;IACnDL,KAAK,EAAE,CACLM,MAAM,CAACC,OAAO,EACd;MACEC,eAAe,EAAEN,KAAK,CAACO,MAAM,CAACC,UAAU;MACxCC,OAAO,EAAEZ,MAAM,GAAIG,KAAK,CAACU,MAAM,GAAG,GAAG,GAAG,GAAG,GAAI;IACjD,CAAC,EACDZ,KAAK;EACL,CACH,CAAC;AAEN;AAEAF,WAAW,CAACe,WAAW,GAAG,aAAa;AAEvC,MAAMP,MAAM,GAAGlB,UAAU,CAAC0B,MAAM,CAAC;EAC/BP,OAAO,EAAE;IACPQ,QAAQ,EAAE,UAAU;IACpBC,GAAG,EAAE,CAAC;IACNC,IAAI,EAAE,CAAC;IACPC,KAAK,EAAE,CAAC;IACRC,MAAM,EAAE,CAAC;IACTC,aAAa,EAAE;EACjB;AACF,CAAC,CAAC","ignoreList":[]}
@@ -1,19 +1,19 @@
1
1
  import React from 'react';
2
2
  import { type PressableProps, type StyleProp, type View, type ViewStyle } from 'react-native';
3
- export interface PressableScaleProps extends Omit<PressableProps, 'children' | 'style'> {
3
+ export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
4
4
  /** Scale applied while pressed. Defaults to `0.98`. */
5
5
  targetScale?: number;
6
- /** Style for the outer, full-size touch target (stays put while pressing). */
6
+ /** Style applied to the pressable itself the node that scales. */
7
7
  style?: StyleProp<ViewStyle>;
8
- /** Style for the inner content wrapper that actually scales. */
9
- contentContainerStyle?: StyleProp<ViewStyle>;
10
- children?: React.ReactNode;
11
8
  }
12
9
  /**
13
- * A drop-in {@link Pressable} whose content springs to a smaller scale on
14
- * press-in and back on release. The outer pressable stays full size (so the
15
- * touch target never shrinks) while an inner wrapper scales.
10
+ * A drop-in {@link Pressable} that springs to a smaller scale on press-in and
11
+ * back on release. The scale is applied to the pressable node itself, so a
12
+ * consumer's `className` / layout styles (e.g. `flex-row items-center`) land on
13
+ * the same node that animates.
16
14
  *
15
+ * The touch target scales with the content — acceptable at the default ~0.98
16
+ * (the earlier two-node touch-target-preservation was dropped for simplicity).
17
17
  * Honours the OS "reduce motion" setting and disables the effect on non-touch
18
18
  * web pointers.
19
19
  */
@@ -1 +1 @@
1
- {"version":3,"file":"PressableScale.d.ts","sourceRoot":"","sources":["../../../../src/pressable-scale/PressableScale.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AA0BtB,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,OAAO,CAAC;IAClD,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,gEAAgE;IAChE,qBAAqB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,kFA+C1B,CAAC"}
1
+ {"version":3,"file":"PressableScale.d.ts","sourceRoot":"","sources":["../../../../src/pressable-scale/PressableScale.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgCtB,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC;IACxE,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,kFAgC1B,CAAC"}
@@ -1,31 +1,36 @@
1
1
  import { type StyleProp, type ViewStyle } from 'react-native';
2
2
  export interface SubtleHoverProps {
3
- /** Whether the parent is currently hovered. Drives the overlay opacity. */
4
- hover: boolean;
5
3
  /**
6
- * Overlay opacity when hovered. Defaults to a mode-tuned value (0.4 dark /
7
- * 0.5 light) that reads as a gentle contrast wash.
4
+ * JS-driven visibility for a coordinated highlight e.g. every post of one
5
+ * thread lighting up together. When omitted, the wash instead follows CSS
6
+ * `group-hover` on web with ZERO React state: the parent element must carry
7
+ * `className="group"`.
8
8
  */
9
- opacity?: number;
9
+ active?: boolean;
10
10
  /** Extra style merged last — e.g. `borderRadius` to match the parent. */
11
11
  style?: StyleProp<ViewStyle>;
12
12
  /**
13
- * Opt in to rendering on native. Off by default: hover is a pointer
14
- * affordance, so this is a web-only overlay unless a native surface wants the
15
- * same wash driven by another interaction (e.g. press).
13
+ * Render on native too (default `false`). Hover is a web pointer affordance,
14
+ * so the CSS `group-hover` mode is web-only; native only shows the wash in the
15
+ * JS `active` mode, and only when this is set.
16
16
  */
17
17
  native?: boolean;
18
18
  }
19
19
  /**
20
- * An absolutely-positioned, non-interactive contrast wash that fades in while
21
- * its parent is hovered. Ported from Bluesky's `SubtleHover`. Pairs naturally
22
- * with {@link useInteractionState} — pass its `state` as `hover`.
20
+ * An absolutely-positioned, non-interactive contrast wash. Two modes:
23
21
  *
24
- * Renders nothing on touch-web (no hovering pointer) and on native unless
25
- * `native` is set. Color comes from the theme's `contrast50` token so the wash
26
- * tracks the active preset and light/dark mode.
22
+ * - **CSS `group-hover`** (`active` omitted): the wash fades in on hover purely
23
+ * through NativeWind classes no React state. The parent must carry
24
+ * `className="group"`. Web-only (native and touch-web have no hover).
25
+ * - **JS `active`**: the wash opacity follows the boolean, so several elements
26
+ * can light up together (e.g. every post of a thread). Web always; native
27
+ * only when `native` is set.
28
+ *
29
+ * The wash color comes from the theme's `contrast50` token so it tracks the
30
+ * active preset and light/dark mode. The web cross-fade is a NativeWind
31
+ * `transition-opacity` class — no manual `willChange` / web-style casts.
27
32
  */
28
- export declare function SubtleHover({ hover, opacity, style, native }: SubtleHoverProps): import("react/jsx-runtime").JSX.Element | null;
33
+ export declare function SubtleHover({ active, style, native }: SubtleHoverProps): import("react/jsx-runtime").JSX.Element | null;
29
34
  export declare namespace SubtleHover {
30
35
  var displayName: string;
31
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SubtleHover.d.ts","sourceRoot":"","sources":["../../../../src/subtle-hover/SubtleHover.tsx"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA4B1F,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAc,EAAE,EAAE,gBAAgB,kDAoBtF;yBApBe,WAAW"}
1
+ {"version":3,"file":"SubtleHover.d.ts","sourceRoot":"","sources":["../../../../src/subtle-hover/SubtleHover.tsx"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAe1F,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAc,EAAE,EAAE,gBAAgB,kDAoC9E;yBApCe,WAAW"}
@@ -1,19 +1,19 @@
1
1
  import React from 'react';
2
2
  import { type PressableProps, type StyleProp, type View, type ViewStyle } from 'react-native';
3
- export interface PressableScaleProps extends Omit<PressableProps, 'children' | 'style'> {
3
+ export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
4
4
  /** Scale applied while pressed. Defaults to `0.98`. */
5
5
  targetScale?: number;
6
- /** Style for the outer, full-size touch target (stays put while pressing). */
6
+ /** Style applied to the pressable itself the node that scales. */
7
7
  style?: StyleProp<ViewStyle>;
8
- /** Style for the inner content wrapper that actually scales. */
9
- contentContainerStyle?: StyleProp<ViewStyle>;
10
- children?: React.ReactNode;
11
8
  }
12
9
  /**
13
- * A drop-in {@link Pressable} whose content springs to a smaller scale on
14
- * press-in and back on release. The outer pressable stays full size (so the
15
- * touch target never shrinks) while an inner wrapper scales.
10
+ * A drop-in {@link Pressable} that springs to a smaller scale on press-in and
11
+ * back on release. The scale is applied to the pressable node itself, so a
12
+ * consumer's `className` / layout styles (e.g. `flex-row items-center`) land on
13
+ * the same node that animates.
16
14
  *
15
+ * The touch target scales with the content — acceptable at the default ~0.98
16
+ * (the earlier two-node touch-target-preservation was dropped for simplicity).
17
17
  * Honours the OS "reduce motion" setting and disables the effect on non-touch
18
18
  * web pointers.
19
19
  */
@@ -1 +1 @@
1
- {"version":3,"file":"PressableScale.d.ts","sourceRoot":"","sources":["../../../../src/pressable-scale/PressableScale.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AA0BtB,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,cAAc,EAAE,UAAU,GAAG,OAAO,CAAC;IAClD,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,gEAAgE;IAChE,qBAAqB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7C,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,kFA+C1B,CAAC"}
1
+ {"version":3,"file":"PressableScale.d.ts","sourceRoot":"","sources":["../../../../src/pressable-scale/PressableScale.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,IAAI,EACT,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgCtB,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC;IACxE,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,kFAgC1B,CAAC"}
@@ -1,31 +1,36 @@
1
1
  import { type StyleProp, type ViewStyle } from 'react-native';
2
2
  export interface SubtleHoverProps {
3
- /** Whether the parent is currently hovered. Drives the overlay opacity. */
4
- hover: boolean;
5
3
  /**
6
- * Overlay opacity when hovered. Defaults to a mode-tuned value (0.4 dark /
7
- * 0.5 light) that reads as a gentle contrast wash.
4
+ * JS-driven visibility for a coordinated highlight e.g. every post of one
5
+ * thread lighting up together. When omitted, the wash instead follows CSS
6
+ * `group-hover` on web with ZERO React state: the parent element must carry
7
+ * `className="group"`.
8
8
  */
9
- opacity?: number;
9
+ active?: boolean;
10
10
  /** Extra style merged last — e.g. `borderRadius` to match the parent. */
11
11
  style?: StyleProp<ViewStyle>;
12
12
  /**
13
- * Opt in to rendering on native. Off by default: hover is a pointer
14
- * affordance, so this is a web-only overlay unless a native surface wants the
15
- * same wash driven by another interaction (e.g. press).
13
+ * Render on native too (default `false`). Hover is a web pointer affordance,
14
+ * so the CSS `group-hover` mode is web-only; native only shows the wash in the
15
+ * JS `active` mode, and only when this is set.
16
16
  */
17
17
  native?: boolean;
18
18
  }
19
19
  /**
20
- * An absolutely-positioned, non-interactive contrast wash that fades in while
21
- * its parent is hovered. Ported from Bluesky's `SubtleHover`. Pairs naturally
22
- * with {@link useInteractionState} — pass its `state` as `hover`.
20
+ * An absolutely-positioned, non-interactive contrast wash. Two modes:
23
21
  *
24
- * Renders nothing on touch-web (no hovering pointer) and on native unless
25
- * `native` is set. Color comes from the theme's `contrast50` token so the wash
26
- * tracks the active preset and light/dark mode.
22
+ * - **CSS `group-hover`** (`active` omitted): the wash fades in on hover purely
23
+ * through NativeWind classes no React state. The parent must carry
24
+ * `className="group"`. Web-only (native and touch-web have no hover).
25
+ * - **JS `active`**: the wash opacity follows the boolean, so several elements
26
+ * can light up together (e.g. every post of a thread). Web always; native
27
+ * only when `native` is set.
28
+ *
29
+ * The wash color comes from the theme's `contrast50` token so it tracks the
30
+ * active preset and light/dark mode. The web cross-fade is a NativeWind
31
+ * `transition-opacity` class — no manual `willChange` / web-style casts.
27
32
  */
28
- export declare function SubtleHover({ hover, opacity, style, native }: SubtleHoverProps): import("react/jsx-runtime").JSX.Element | null;
33
+ export declare function SubtleHover({ active, style, native }: SubtleHoverProps): import("react/jsx-runtime").JSX.Element | null;
29
34
  export declare namespace SubtleHover {
30
35
  var displayName: string;
31
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SubtleHover.d.ts","sourceRoot":"","sources":["../../../../src/subtle-hover/SubtleHover.tsx"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA4B1F,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAc,EAAE,EAAE,gBAAgB,kDAoBtF;yBApBe,WAAW"}
1
+ {"version":3,"file":"SubtleHover.d.ts","sourceRoot":"","sources":["../../../../src/subtle-hover/SubtleHover.tsx"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAe1F,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAc,EAAE,EAAE,gBAAgB,kDAoC9E;yBApCe,WAAW"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,4 +26,13 @@ describe('PressableScale', () => {
26
26
  );
27
27
  expect(getByText('Scaled')).toBeTruthy();
28
28
  });
29
+
30
+ it('applies style to the pressable node that scales', () => {
31
+ const { getByText } = render(
32
+ <PressableScale style={{ flexDirection: 'row' }} accessibilityLabel="Row">
33
+ <Text>Row content</Text>
34
+ </PressableScale>,
35
+ );
36
+ expect(getByText('Row content')).toBeTruthy();
37
+ });
29
38
  });
@@ -5,7 +5,8 @@ import { BloomThemeProvider } from '../theme/BloomThemeProvider';
5
5
  import { SubtleHover } from '../subtle-hover';
6
6
 
7
7
  // Default test Platform.OS is 'ios' (see __mocks__/react-native.ts), so the
8
- // web-only overlay is suppressed unless `native` is opted in.
8
+ // web-only hover wash is suppressed unless the JS `active` mode opts in via
9
+ // `native`.
9
10
  function renderWithTheme(ui: React.ReactElement) {
10
11
  return render(
11
12
  <BloomThemeProvider mode="light" colorPreset="teal">
@@ -14,29 +15,36 @@ function renderWithTheme(ui: React.ReactElement) {
14
15
  );
15
16
  }
16
17
 
18
+ // The provider wraps the subject in ONE flex View, so `toJSON` is a single node;
19
+ // narrowing away the array/null branches lets `children` read the wash cleanly.
20
+ function renderWrapper(ui: React.ReactElement) {
21
+ const tree = renderWithTheme(ui).toJSON();
22
+ if (tree === null || Array.isArray(tree)) {
23
+ throw new Error('expected a single wrapper node');
24
+ }
25
+ return tree;
26
+ }
27
+
17
28
  describe('SubtleHover', () => {
18
29
  it('carries a displayName', () => {
19
30
  expect(SubtleHover.displayName).toBe('SubtleHover');
20
31
  });
21
32
 
22
- // `renderWithTheme` wraps the subject in the provider's flex View, so the
23
- // overlay's presence is read from that wrapper's `children` rather than the
24
- // root being null.
25
- it('renders nothing on native by default', () => {
26
- const tree = renderWithTheme(<SubtleHover hover />).toJSON();
27
- expect(tree).not.toBeNull();
28
- expect(tree?.children).toBeNull();
33
+ it('renders nothing on native in the CSS group-hover mode', () => {
34
+ expect(renderWrapper(<SubtleHover />).children).toBeNull();
35
+ });
36
+
37
+ it('renders nothing on native in the JS active mode by default', () => {
38
+ expect(renderWrapper(<SubtleHover active />).children).toBeNull();
29
39
  });
30
40
 
31
- it('renders the overlay on native when opted in', () => {
32
- const tree = renderWithTheme(<SubtleHover hover native />).toJSON();
33
- expect(tree?.children).not.toBeNull();
41
+ it('renders the wash on native in the JS active mode when opted in', () => {
42
+ expect(renderWrapper(<SubtleHover active native />).children).not.toBeNull();
34
43
  });
35
44
 
36
- it('accepts an opacity override and a style', () => {
37
- const tree = renderWithTheme(
38
- <SubtleHover hover native opacity={0.9} style={{ borderRadius: 12 }} />,
39
- ).toJSON();
40
- expect(tree?.children).not.toBeNull();
45
+ it('accepts a style override in the native active mode', () => {
46
+ expect(
47
+ renderWrapper(<SubtleHover active native style={{ borderRadius: 12 }} />).children,
48
+ ).not.toBeNull();
41
49
  });
42
50
  });
@@ -21,7 +21,7 @@ export const Basic: Story = {
21
21
  <PressableScale
22
22
  {...args}
23
23
  accessibilityLabel="Press me"
24
- contentContainerStyle={{
24
+ style={{
25
25
  paddingVertical: 12,
26
26
  paddingHorizontal: 20,
27
27
  borderRadius: 12,
@@ -39,7 +39,7 @@ export const StrongScale: Story = {
39
39
  <PressableScale
40
40
  {...args}
41
41
  accessibilityLabel="Press me harder"
42
- contentContainerStyle={{
42
+ style={{
43
43
  paddingVertical: 12,
44
44
  paddingHorizontal: 20,
45
45
  borderRadius: 12,
@@ -51,6 +51,27 @@ export const StrongScale: Story = {
51
51
  ),
52
52
  };
53
53
 
54
+ /**
55
+ * NativeWind layout classes apply to the SAME node that scales — the whole row
56
+ * (avatar + label) springs as one. In a NativeWind consumer you pass `className`
57
+ * directly; the spread-cast here just satisfies Bloom's own typecheck (which has
58
+ * no NativeWind ambient types).
59
+ */
60
+ export const InlineRow: Story = {
61
+ render: (args) => (
62
+ <PressableScale
63
+ {...args}
64
+ accessibilityLabel="Open profile"
65
+ {...({
66
+ className: 'flex-row items-center gap-3 px-4 py-3 rounded-xl bg-muted',
67
+ } as Record<string, string>)}
68
+ >
69
+ <View style={{ width: 32, height: 32, borderRadius: 16, backgroundColor: '#4f46e5' }} />
70
+ <Text style={{ fontWeight: '600' }}>Row that scales as one node</Text>
71
+ </PressableScale>
72
+ ),
73
+ };
74
+
54
75
  export const Card: Story = {
55
76
  render: (args) => (
56
77
  <PressableScale {...args} accessibilityLabel="Open card">
@@ -32,38 +32,32 @@ const IS_WEB_TOUCH_DEVICE =
32
32
 
33
33
  const SCALE_SUPPORTED = Platform.OS !== 'web' || IS_WEB_TOUCH_DEVICE;
34
34
 
35
- export interface PressableScaleProps
36
- extends Omit<PressableProps, 'children' | 'style'> {
35
+ /**
36
+ * A single animated `Pressable`, built once at module scope so the animated
37
+ * node identity is stable across renders.
38
+ */
39
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
40
+
41
+ export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
37
42
  /** Scale applied while pressed. Defaults to `0.98`. */
38
43
  targetScale?: number;
39
- /** Style for the outer, full-size touch target (stays put while pressing). */
44
+ /** Style applied to the pressable itself the node that scales. */
40
45
  style?: StyleProp<ViewStyle>;
41
- /** Style for the inner content wrapper that actually scales. */
42
- contentContainerStyle?: StyleProp<ViewStyle>;
43
- children?: React.ReactNode;
44
46
  }
45
47
 
46
48
  /**
47
- * A drop-in {@link Pressable} whose content springs to a smaller scale on
48
- * press-in and back on release. The outer pressable stays full size (so the
49
- * touch target never shrinks) while an inner wrapper scales.
49
+ * A drop-in {@link Pressable} that springs to a smaller scale on press-in and
50
+ * back on release. The scale is applied to the pressable node itself, so a
51
+ * consumer's `className` / layout styles (e.g. `flex-row items-center`) land on
52
+ * the same node that animates.
50
53
  *
54
+ * The touch target scales with the content — acceptable at the default ~0.98
55
+ * (the earlier two-node touch-target-preservation was dropped for simplicity).
51
56
  * Honours the OS "reduce motion" setting and disables the effect on non-touch
52
57
  * web pointers.
53
58
  */
54
59
  export const PressableScale = forwardRef<View, PressableScaleProps>(
55
- function PressableScale(
56
- {
57
- targetScale = 0.98,
58
- style,
59
- contentContainerStyle,
60
- children,
61
- onPressIn,
62
- onPressOut,
63
- ...rest
64
- },
65
- ref,
66
- ) {
60
+ function PressableScale({ targetScale = 0.98, style, onPressIn, onPressOut, ...rest }, ref) {
67
61
  const reducedMotion = useReducedMotion();
68
62
  const animate = SCALE_SUPPORTED && !reducedMotion;
69
63
 
@@ -74,7 +68,7 @@ export const PressableScale = forwardRef<View, PressableScaleProps>(
74
68
  }));
75
69
 
76
70
  return (
77
- <Pressable
71
+ <AnimatedPressable
78
72
  ref={ref}
79
73
  accessibilityRole="button"
80
74
  onPressIn={(e) => {
@@ -89,13 +83,9 @@ export const PressableScale = forwardRef<View, PressableScaleProps>(
89
83
  cancelAnimation(scale);
90
84
  scale.value = withTiming(1, { duration: DURATION });
91
85
  }}
92
- style={style}
86
+ style={[animate ? animatedStyle : null, style]}
93
87
  {...rest}
94
- >
95
- <Animated.View style={[animate ? animatedStyle : null, contentContainerStyle]}>
96
- {children}
97
- </Animated.View>
98
- </Pressable>
88
+ />
99
89
  );
100
90
  },
101
91
  );
@@ -14,15 +14,15 @@ export default meta;
14
14
 
15
15
  type Story = StoryObj<typeof SubtleHover>;
16
16
 
17
- /** A row that washes its background as the pointer enters (web). */
18
- function HoverRow({ label, native }: { label: string; native?: boolean }) {
19
- const { state: hovered, onIn, onOut } = useInteractionState();
20
-
17
+ /**
18
+ * Pure-CSS `group-hover`: the parent carries `className="group"` and the wash
19
+ * fades in on hover with ZERO React state (web only).
20
+ */
21
+ function HoverRow({ label }: { label: string }) {
21
22
  return (
22
23
  <Pressable
24
+ {...({ className: 'group' } as Record<string, string>)}
23
25
  accessibilityLabel={label}
24
- onHoverIn={onIn}
25
- onHoverOut={onOut}
26
26
  style={{
27
27
  width: 280,
28
28
  paddingVertical: 14,
@@ -31,34 +31,14 @@ function HoverRow({ label, native }: { label: string; native?: boolean }) {
31
31
  overflow: 'hidden',
32
32
  }}
33
33
  >
34
- <SubtleHover hover={hovered} native={native} style={{ borderRadius: 12 }} />
34
+ <SubtleHover style={{ borderRadius: 12 }} />
35
35
  <Text style={{ fontWeight: '600' }}>{label}</Text>
36
36
  </Pressable>
37
37
  );
38
38
  }
39
39
 
40
- export const ListRow: Story = {
41
- render: () => <HoverRow label="Hover me (web)" />,
42
- };
43
-
44
- export const StrongerWash: Story = {
45
- render: () => {
46
- function Row() {
47
- const { state: hovered, onIn, onOut } = useInteractionState();
48
- return (
49
- <Pressable
50
- accessibilityLabel="Stronger wash"
51
- onHoverIn={onIn}
52
- onHoverOut={onOut}
53
- style={{ width: 280, padding: 16, borderRadius: 12, overflow: 'hidden' }}
54
- >
55
- <SubtleHover hover={hovered} opacity={0.85} style={{ borderRadius: 12 }} />
56
- <Text style={{ fontWeight: '600' }}>Higher opacity override</Text>
57
- </Pressable>
58
- );
59
- }
60
- return <Row />;
61
- },
40
+ export const GroupHover: Story = {
41
+ render: () => <HoverRow label="Hover me (web, group-hover)" />,
62
42
  };
63
43
 
64
44
  export const Stack: Story = {
@@ -70,3 +50,36 @@ export const Stack: Story = {
70
50
  </View>
71
51
  ),
72
52
  };
53
+
54
+ /**
55
+ * JS `active` mode: hovering anywhere in the thread lights EVERY row together
56
+ * (a coordinated highlight a per-element `group-hover` can't express). `native`
57
+ * is opted in so the same wash shows on native surfaces too.
58
+ */
59
+ export const CoordinatedThread: Story = {
60
+ render: () => {
61
+ function Thread() {
62
+ const { state: hovered, onIn, onOut } = useInteractionState();
63
+ return (
64
+ <Pressable onHoverIn={onIn} onHoverOut={onOut} style={{ gap: 4 }}>
65
+ {['First reply', 'Second reply', 'Third reply'].map((label) => (
66
+ <View
67
+ key={label}
68
+ style={{
69
+ width: 280,
70
+ paddingVertical: 14,
71
+ paddingHorizontal: 16,
72
+ borderRadius: 12,
73
+ overflow: 'hidden',
74
+ }}
75
+ >
76
+ <SubtleHover active={hovered} native style={{ borderRadius: 12 }} />
77
+ <Text style={{ fontWeight: '600' }}>{label}</Text>
78
+ </View>
79
+ ))}
80
+ </Pressable>
81
+ );
82
+ }
83
+ return <Thread />;
84
+ },
85
+ };
@@ -4,9 +4,9 @@ import { Platform, StyleSheet, View, type StyleProp, type ViewStyle } from 'reac
4
4
  import { useTheme } from '../theme';
5
5
 
6
6
  /**
7
- * True on touch-capable web browsers (coarse pointer). A hover wash only makes
8
- * sense with a hovering pointer, so it is suppressed on touch web to match
9
- * native (which has no hover at all).
7
+ * True on touch-capable web browsers (coarse pointer). The CSS `group-hover`
8
+ * wash only makes sense with a hovering pointer, so it is suppressed on touch
9
+ * web to match native (which has no hover at all).
10
10
  */
11
11
  const IS_WEB_TOUCH_DEVICE =
12
12
  Platform.OS === 'web' &&
@@ -14,62 +14,70 @@ const IS_WEB_TOUCH_DEVICE =
14
14
  typeof window.matchMedia === 'function' &&
15
15
  window.matchMedia('(pointer: coarse)').matches;
16
16
 
17
- /**
18
- * Web-only style that (a) cross-fades the overlay opacity and (b) promotes it to
19
- * its own GPU layer so Safari doesn't nudge the layout by a subpixel when the
20
- * overlay composites on hover. Cast because RN's `ViewStyle` has no `willChange`
21
- * / `transition*` keys; react-native-web forwards them to the DOM node.
22
- */
23
- const WEB_TRANSITION = {
24
- transitionProperty: 'opacity',
25
- transitionDuration: '0.1s',
26
- transitionTimingFunction: 'ease',
27
- willChange: 'opacity',
28
- } as ViewStyle;
29
-
30
17
  export interface SubtleHoverProps {
31
- /** Whether the parent is currently hovered. Drives the overlay opacity. */
32
- hover: boolean;
33
18
  /**
34
- * Overlay opacity when hovered. Defaults to a mode-tuned value (0.4 dark /
35
- * 0.5 light) that reads as a gentle contrast wash.
19
+ * JS-driven visibility for a coordinated highlight e.g. every post of one
20
+ * thread lighting up together. When omitted, the wash instead follows CSS
21
+ * `group-hover` on web with ZERO React state: the parent element must carry
22
+ * `className="group"`.
36
23
  */
37
- opacity?: number;
24
+ active?: boolean;
38
25
  /** Extra style merged last — e.g. `borderRadius` to match the parent. */
39
26
  style?: StyleProp<ViewStyle>;
40
27
  /**
41
- * Opt in to rendering on native. Off by default: hover is a pointer
42
- * affordance, so this is a web-only overlay unless a native surface wants the
43
- * same wash driven by another interaction (e.g. press).
28
+ * Render on native too (default `false`). Hover is a web pointer affordance,
29
+ * so the CSS `group-hover` mode is web-only; native only shows the wash in the
30
+ * JS `active` mode, and only when this is set.
44
31
  */
45
32
  native?: boolean;
46
33
  }
47
34
 
48
35
  /**
49
- * An absolutely-positioned, non-interactive contrast wash that fades in while
50
- * its parent is hovered. Ported from Bluesky's `SubtleHover`. Pairs naturally
51
- * with {@link useInteractionState} pass its `state` as `hover`.
36
+ * An absolutely-positioned, non-interactive contrast wash. Two modes:
37
+ *
38
+ * - **CSS `group-hover`** (`active` omitted): the wash fades in on hover purely
39
+ * through NativeWind classes — no React state. The parent must carry
40
+ * `className="group"`. Web-only (native and touch-web have no hover).
41
+ * - **JS `active`**: the wash opacity follows the boolean, so several elements
42
+ * can light up together (e.g. every post of a thread). Web always; native
43
+ * only when `native` is set.
52
44
  *
53
- * Renders nothing on touch-web (no hovering pointer) and on native unless
54
- * `native` is set. Color comes from the theme's `contrast50` token so the wash
55
- * tracks the active preset and light/dark mode.
45
+ * The wash color comes from the theme's `contrast50` token so it tracks the
46
+ * active preset and light/dark mode. The web cross-fade is a NativeWind
47
+ * `transition-opacity` class no manual `willChange` / web-style casts.
56
48
  */
57
- export function SubtleHover({ hover, opacity, style, native = false }: SubtleHoverProps) {
49
+ export function SubtleHover({ active, style, native = false }: SubtleHoverProps) {
58
50
  const theme = useTheme();
59
-
60
51
  const isWeb = Platform.OS === 'web';
61
- if (isWeb ? IS_WEB_TOUCH_DEVICE : !native) {
62
- return null;
63
- }
64
52
 
65
- const targetOpacity = opacity ?? (theme.isDark ? 0.4 : 0.5);
53
+ // Full literal class strings (never concatenated fragments) so NativeWind's
54
+ // compiler statically extracts every variant.
55
+ if (active === undefined) {
56
+ // CSS group-hover: a web pointer affordance. Native has no hover, and
57
+ // neither does touch web — render nothing there.
58
+ if (!isWeb || IS_WEB_TOUCH_DEVICE) return null;
59
+ return (
60
+ <View
61
+ {...({
62
+ className:
63
+ 'opacity-0 group-hover:opacity-50 dark:group-hover:opacity-40 transition-opacity duration-150',
64
+ } as Record<string, string>)}
65
+ style={[styles.overlay, { backgroundColor: theme.colors.contrast50 }, style]}
66
+ />
67
+ );
68
+ }
66
69
 
70
+ // JS-driven coordinated mode: web always; native only when opted in.
71
+ if (!isWeb && !native) return null;
67
72
  return (
68
73
  <View
74
+ {...({ className: 'transition-opacity duration-150' } as Record<string, string>)}
69
75
  style={[
70
76
  styles.overlay,
71
- { backgroundColor: theme.colors.contrast50, opacity: hover ? targetOpacity : 0 },
72
- isWeb ? WEB_TRANSITION : null,
77
+ {
78
+ backgroundColor: theme.colors.contrast50,
79
+ opacity: active ? (theme.isDark ? 0.4 : 0.5) : 0,
80
+ },
73
81
  style,
74
82
  ]}
75
83
  />