@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.
- package/lib/commonjs/pressable-scale/PressableScale.js +15 -12
- package/lib/commonjs/pressable-scale/PressableScale.js.map +1 -1
- package/lib/commonjs/subtle-hover/SubtleHover.js +34 -29
- package/lib/commonjs/subtle-hover/SubtleHover.js.map +1 -1
- package/lib/module/pressable-scale/PressableScale.js +15 -12
- package/lib/module/pressable-scale/PressableScale.js.map +1 -1
- package/lib/module/subtle-hover/SubtleHover.js +34 -29
- package/lib/module/subtle-hover/SubtleHover.js.map +1 -1
- package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts +8 -8
- package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts.map +1 -1
- package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts +20 -15
- package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts.map +1 -1
- package/lib/typescript/module/pressable-scale/PressableScale.d.ts +8 -8
- package/lib/typescript/module/pressable-scale/PressableScale.d.ts.map +1 -1
- package/lib/typescript/module/subtle-hover/SubtleHover.d.ts +20 -15
- package/lib/typescript/module/subtle-hover/SubtleHover.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/PressableScale.test.tsx +9 -0
- package/src/__tests__/SubtleHover.test.tsx +24 -16
- package/src/pressable-scale/PressableScale.stories.tsx +23 -2
- package/src/pressable-scale/PressableScale.tsx +18 -28
- package/src/subtle-hover/SubtleHover.stories.tsx +42 -29
- package/src/subtle-hover/SubtleHover.tsx +46 -38
|
@@ -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}
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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)(
|
|
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","
|
|
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).
|
|
14
|
-
* sense with a hovering pointer, so it is suppressed on touch
|
|
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
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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:
|
|
55
|
-
},
|
|
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","
|
|
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}
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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(
|
|
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","
|
|
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).
|
|
9
|
-
* sense with a hovering pointer, so it is suppressed on touch
|
|
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
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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:
|
|
52
|
-
},
|
|
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","
|
|
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, '
|
|
3
|
+
export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
|
|
4
4
|
/** Scale applied while pressed. Defaults to `0.98`. */
|
|
5
5
|
targetScale?: number;
|
|
6
|
-
/** Style
|
|
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}
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
9
|
+
active?: boolean;
|
|
10
10
|
/** Extra style merged last — e.g. `borderRadius` to match the parent. */
|
|
11
11
|
style?: StyleProp<ViewStyle>;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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({
|
|
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;
|
|
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, '
|
|
3
|
+
export interface PressableScaleProps extends Omit<PressableProps, 'style'> {
|
|
4
4
|
/** Scale applied while pressed. Defaults to `0.98`. */
|
|
5
5
|
targetScale?: number;
|
|
6
|
-
/** Style
|
|
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}
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
9
|
+
active?: boolean;
|
|
10
10
|
/** Extra style merged last — e.g. `borderRadius` to match the parent. */
|
|
11
11
|
style?: StyleProp<ViewStyle>;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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({
|
|
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;
|
|
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
|
@@ -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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
expect(
|
|
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
|
|
32
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
<SubtleHover
|
|
39
|
-
).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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}
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
<
|
|
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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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).
|
|
8
|
-
* sense with a hovering pointer, so it is suppressed on touch
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
-
|
|
24
|
+
active?: boolean;
|
|
38
25
|
/** Extra style merged last — e.g. `borderRadius` to match the parent. */
|
|
39
26
|
style?: StyleProp<ViewStyle>;
|
|
40
27
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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({
|
|
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
|
-
|
|
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
|
-
{
|
|
72
|
-
|
|
77
|
+
{
|
|
78
|
+
backgroundColor: theme.colors.contrast50,
|
|
79
|
+
opacity: active ? (theme.isDark ? 0.4 : 0.5) : 0,
|
|
80
|
+
},
|
|
73
81
|
style,
|
|
74
82
|
]}
|
|
75
83
|
/>
|