@usels/core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +21 -0
  2. package/dist/browser/useEventListener/index.d.mts +56 -0
  3. package/dist/browser/useEventListener/index.d.ts +56 -0
  4. package/dist/browser/useEventListener/index.js +112 -0
  5. package/dist/browser/useEventListener/index.js.map +1 -0
  6. package/dist/browser/useEventListener/index.mjs +88 -0
  7. package/dist/browser/useEventListener/index.mjs.map +1 -0
  8. package/dist/browser/useMediaQuery/demo.d.mts +5 -0
  9. package/dist/browser/useMediaQuery/demo.d.ts +5 -0
  10. package/dist/browser/useMediaQuery/demo.js +83 -0
  11. package/dist/browser/useMediaQuery/demo.js.map +1 -0
  12. package/dist/browser/useMediaQuery/demo.mjs +63 -0
  13. package/dist/browser/useMediaQuery/demo.mjs.map +1 -0
  14. package/dist/browser/useMediaQuery/index.d.mts +11 -0
  15. package/dist/browser/useMediaQuery/index.d.ts +11 -0
  16. package/dist/browser/useMediaQuery/index.js +89 -0
  17. package/dist/browser/useMediaQuery/index.js.map +1 -0
  18. package/dist/browser/useMediaQuery/index.mjs +64 -0
  19. package/dist/browser/useMediaQuery/index.mjs.map +1 -0
  20. package/dist/components/Auto/index.d.mts +33 -0
  21. package/dist/components/Auto/index.d.ts +33 -0
  22. package/dist/components/Auto/index.js +66 -0
  23. package/dist/components/Auto/index.js.map +1 -0
  24. package/dist/components/Auto/index.mjs +34 -0
  25. package/dist/components/Auto/index.mjs.map +1 -0
  26. package/dist/elements/useDocumentVisibility/demo.d.mts +5 -0
  27. package/dist/elements/useDocumentVisibility/demo.d.ts +5 -0
  28. package/dist/elements/useDocumentVisibility/demo.js +130 -0
  29. package/dist/elements/useDocumentVisibility/demo.js.map +1 -0
  30. package/dist/elements/useDocumentVisibility/demo.mjs +114 -0
  31. package/dist/elements/useDocumentVisibility/demo.mjs.map +1 -0
  32. package/dist/elements/useDocumentVisibility/index.d.mts +5 -0
  33. package/dist/elements/useDocumentVisibility/index.d.ts +5 -0
  34. package/dist/elements/useDocumentVisibility/index.js +45 -0
  35. package/dist/elements/useDocumentVisibility/index.js.map +1 -0
  36. package/dist/elements/useDocumentVisibility/index.mjs +21 -0
  37. package/dist/elements/useDocumentVisibility/index.mjs.map +1 -0
  38. package/dist/elements/useElementBounding/demo.d.mts +5 -0
  39. package/dist/elements/useElementBounding/demo.d.ts +5 -0
  40. package/dist/elements/useElementBounding/demo.js +87 -0
  41. package/dist/elements/useElementBounding/demo.js.map +1 -0
  42. package/dist/elements/useElementBounding/demo.mjs +67 -0
  43. package/dist/elements/useElementBounding/demo.mjs.map +1 -0
  44. package/dist/elements/useElementBounding/index.d.mts +46 -0
  45. package/dist/elements/useElementBounding/index.d.ts +46 -0
  46. package/dist/elements/useElementBounding/index.js +122 -0
  47. package/dist/elements/useElementBounding/index.js.map +1 -0
  48. package/dist/elements/useElementBounding/index.mjs +98 -0
  49. package/dist/elements/useElementBounding/index.mjs.map +1 -0
  50. package/dist/elements/useElementSize/demo.d.mts +5 -0
  51. package/dist/elements/useElementSize/demo.d.ts +5 -0
  52. package/dist/elements/useElementSize/demo.js +83 -0
  53. package/dist/elements/useElementSize/demo.js.map +1 -0
  54. package/dist/elements/useElementSize/demo.mjs +63 -0
  55. package/dist/elements/useElementSize/demo.mjs.map +1 -0
  56. package/dist/elements/useElementSize/index.d.mts +34 -0
  57. package/dist/elements/useElementSize/index.d.ts +34 -0
  58. package/dist/elements/useElementSize/index.js +85 -0
  59. package/dist/elements/useElementSize/index.js.map +1 -0
  60. package/dist/elements/useElementSize/index.mjs +61 -0
  61. package/dist/elements/useElementSize/index.mjs.map +1 -0
  62. package/dist/elements/useElementVisibility/demo.d.mts +5 -0
  63. package/dist/elements/useElementVisibility/demo.d.ts +5 -0
  64. package/dist/elements/useElementVisibility/demo.js +110 -0
  65. package/dist/elements/useElementVisibility/demo.js.map +1 -0
  66. package/dist/elements/useElementVisibility/demo.mjs +90 -0
  67. package/dist/elements/useElementVisibility/demo.mjs.map +1 -0
  68. package/dist/elements/useElementVisibility/index.d.mts +43 -0
  69. package/dist/elements/useElementVisibility/index.d.ts +43 -0
  70. package/dist/elements/useElementVisibility/index.js +58 -0
  71. package/dist/elements/useElementVisibility/index.js.map +1 -0
  72. package/dist/elements/useElementVisibility/index.mjs +34 -0
  73. package/dist/elements/useElementVisibility/index.mjs.map +1 -0
  74. package/dist/elements/useIntersectionObserver/demo.d.mts +5 -0
  75. package/dist/elements/useIntersectionObserver/demo.d.ts +5 -0
  76. package/dist/elements/useIntersectionObserver/demo.js +173 -0
  77. package/dist/elements/useIntersectionObserver/demo.js.map +1 -0
  78. package/dist/elements/useIntersectionObserver/demo.mjs +153 -0
  79. package/dist/elements/useIntersectionObserver/demo.mjs.map +1 -0
  80. package/dist/elements/useIntersectionObserver/index.d.mts +47 -0
  81. package/dist/elements/useIntersectionObserver/index.d.ts +47 -0
  82. package/dist/elements/useIntersectionObserver/index.js +111 -0
  83. package/dist/elements/useIntersectionObserver/index.js.map +1 -0
  84. package/dist/elements/useIntersectionObserver/index.mjs +87 -0
  85. package/dist/elements/useIntersectionObserver/index.mjs.map +1 -0
  86. package/dist/elements/useMouseInElement/demo.d.mts +5 -0
  87. package/dist/elements/useMouseInElement/demo.d.ts +5 -0
  88. package/dist/elements/useMouseInElement/demo.js +104 -0
  89. package/dist/elements/useMouseInElement/demo.js.map +1 -0
  90. package/dist/elements/useMouseInElement/demo.mjs +84 -0
  91. package/dist/elements/useMouseInElement/demo.mjs.map +1 -0
  92. package/dist/elements/useMouseInElement/index.d.mts +56 -0
  93. package/dist/elements/useMouseInElement/index.d.ts +56 -0
  94. package/dist/elements/useMouseInElement/index.js +148 -0
  95. package/dist/elements/useMouseInElement/index.js.map +1 -0
  96. package/dist/elements/useMouseInElement/index.mjs +124 -0
  97. package/dist/elements/useMouseInElement/index.mjs.map +1 -0
  98. package/dist/elements/useMutationObserver/demo.d.mts +5 -0
  99. package/dist/elements/useMutationObserver/demo.d.ts +5 -0
  100. package/dist/elements/useMutationObserver/demo.js +240 -0
  101. package/dist/elements/useMutationObserver/demo.js.map +1 -0
  102. package/dist/elements/useMutationObserver/demo.mjs +220 -0
  103. package/dist/elements/useMutationObserver/demo.mjs.map +1 -0
  104. package/dist/elements/useMutationObserver/index.d.mts +15 -0
  105. package/dist/elements/useMutationObserver/index.d.ts +15 -0
  106. package/dist/elements/useMutationObserver/index.js +69 -0
  107. package/dist/elements/useMutationObserver/index.js.map +1 -0
  108. package/dist/elements/useMutationObserver/index.mjs +45 -0
  109. package/dist/elements/useMutationObserver/index.mjs.map +1 -0
  110. package/dist/elements/useParentElement/demo.d.mts +5 -0
  111. package/dist/elements/useParentElement/demo.d.ts +5 -0
  112. package/dist/elements/useParentElement/demo.js +132 -0
  113. package/dist/elements/useParentElement/demo.js.map +1 -0
  114. package/dist/elements/useParentElement/demo.mjs +112 -0
  115. package/dist/elements/useParentElement/demo.mjs.map +1 -0
  116. package/dist/elements/useParentElement/index.d.mts +7 -0
  117. package/dist/elements/useParentElement/index.d.ts +7 -0
  118. package/dist/elements/useParentElement/index.js +47 -0
  119. package/dist/elements/useParentElement/index.js.map +1 -0
  120. package/dist/elements/useParentElement/index.mjs +23 -0
  121. package/dist/elements/useParentElement/index.mjs.map +1 -0
  122. package/dist/elements/useRef$/index.js +89 -0
  123. package/dist/elements/useRef$/index.js.map +1 -0
  124. package/dist/elements/useRef$/index.mjs +62 -0
  125. package/dist/elements/useRef$/index.mjs.map +1 -0
  126. package/dist/elements/useRef_/index.d.mts +60 -0
  127. package/dist/elements/useRef_/index.d.ts +60 -0
  128. package/dist/elements/useResizeObserver/demo.d.mts +5 -0
  129. package/dist/elements/useResizeObserver/demo.d.ts +5 -0
  130. package/dist/elements/useResizeObserver/demo.js +90 -0
  131. package/dist/elements/useResizeObserver/demo.js.map +1 -0
  132. package/dist/elements/useResizeObserver/demo.mjs +70 -0
  133. package/dist/elements/useResizeObserver/demo.mjs.map +1 -0
  134. package/dist/elements/useResizeObserver/index.d.mts +36 -0
  135. package/dist/elements/useResizeObserver/index.d.ts +36 -0
  136. package/dist/elements/useResizeObserver/index.js +74 -0
  137. package/dist/elements/useResizeObserver/index.js.map +1 -0
  138. package/dist/elements/useResizeObserver/index.mjs +49 -0
  139. package/dist/elements/useResizeObserver/index.mjs.map +1 -0
  140. package/dist/elements/useWindowFocus/demo.d.mts +5 -0
  141. package/dist/elements/useWindowFocus/demo.d.ts +5 -0
  142. package/dist/elements/useWindowFocus/demo.js +104 -0
  143. package/dist/elements/useWindowFocus/demo.js.map +1 -0
  144. package/dist/elements/useWindowFocus/demo.mjs +84 -0
  145. package/dist/elements/useWindowFocus/demo.mjs.map +1 -0
  146. package/dist/elements/useWindowFocus/index.d.mts +5 -0
  147. package/dist/elements/useWindowFocus/index.d.ts +5 -0
  148. package/dist/elements/useWindowFocus/index.js +42 -0
  149. package/dist/elements/useWindowFocus/index.js.map +1 -0
  150. package/dist/elements/useWindowFocus/index.mjs +18 -0
  151. package/dist/elements/useWindowFocus/index.mjs.map +1 -0
  152. package/dist/elements/useWindowSize/demo.d.mts +5 -0
  153. package/dist/elements/useWindowSize/demo.d.ts +5 -0
  154. package/dist/elements/useWindowSize/demo.js +79 -0
  155. package/dist/elements/useWindowSize/demo.js.map +1 -0
  156. package/dist/elements/useWindowSize/demo.mjs +59 -0
  157. package/dist/elements/useWindowSize/demo.mjs.map +1 -0
  158. package/dist/elements/useWindowSize/index.d.mts +17 -0
  159. package/dist/elements/useWindowSize/index.d.ts +17 -0
  160. package/dist/elements/useWindowSize/index.js +96 -0
  161. package/dist/elements/useWindowSize/index.js.map +1 -0
  162. package/dist/elements/useWindowSize/index.mjs +76 -0
  163. package/dist/elements/useWindowSize/index.mjs.map +1 -0
  164. package/dist/function/get/index.d.mts +45 -0
  165. package/dist/function/get/index.d.ts +45 -0
  166. package/dist/function/get/index.js +39 -0
  167. package/dist/function/get/index.js.map +1 -0
  168. package/dist/function/get/index.mjs +15 -0
  169. package/dist/function/get/index.mjs.map +1 -0
  170. package/dist/function/peek/index.d.mts +46 -0
  171. package/dist/function/peek/index.d.ts +46 -0
  172. package/dist/function/peek/index.js +39 -0
  173. package/dist/function/peek/index.js.map +1 -0
  174. package/dist/function/peek/index.mjs +15 -0
  175. package/dist/function/peek/index.mjs.map +1 -0
  176. package/dist/function/useMayObservableOptions/index.d.mts +59 -0
  177. package/dist/function/useMayObservableOptions/index.d.ts +59 -0
  178. package/dist/function/useMayObservableOptions/index.js +109 -0
  179. package/dist/function/useMayObservableOptions/index.js.map +1 -0
  180. package/dist/function/useMayObservableOptions/index.mjs +88 -0
  181. package/dist/function/useMayObservableOptions/index.mjs.map +1 -0
  182. package/dist/function/useSupported/index.d.mts +6 -0
  183. package/dist/function/useSupported/index.d.ts +6 -0
  184. package/dist/function/useSupported/index.js +37 -0
  185. package/dist/function/useSupported/index.js.map +1 -0
  186. package/dist/function/useSupported/index.mjs +13 -0
  187. package/dist/function/useSupported/index.mjs.map +1 -0
  188. package/dist/function/useWhenMounted/index.d.mts +6 -0
  189. package/dist/function/useWhenMounted/index.d.ts +6 -0
  190. package/dist/function/useWhenMounted/index.js +37 -0
  191. package/dist/function/useWhenMounted/index.js.map +1 -0
  192. package/dist/function/useWhenMounted/index.mjs +13 -0
  193. package/dist/function/useWhenMounted/index.mjs.map +1 -0
  194. package/dist/index.d.mts +24 -0
  195. package/dist/index.d.ts +24 -0
  196. package/dist/index.js +63 -0
  197. package/dist/index.js.map +1 -0
  198. package/dist/index.mjs +22 -0
  199. package/dist/index.mjs.map +1 -0
  200. package/dist/sensors/useScroll/demo.d.mts +5 -0
  201. package/dist/sensors/useScroll/demo.d.ts +5 -0
  202. package/dist/sensors/useScroll/demo.js +122 -0
  203. package/dist/sensors/useScroll/demo.js.map +1 -0
  204. package/dist/sensors/useScroll/demo.mjs +102 -0
  205. package/dist/sensors/useScroll/demo.mjs.map +1 -0
  206. package/dist/sensors/useScroll/index.d.mts +42 -0
  207. package/dist/sensors/useScroll/index.d.ts +42 -0
  208. package/dist/sensors/useScroll/index.js +149 -0
  209. package/dist/sensors/useScroll/index.js.map +1 -0
  210. package/dist/sensors/useScroll/index.mjs +125 -0
  211. package/dist/sensors/useScroll/index.mjs.map +1 -0
  212. package/dist/sensors/useWindowScroll/demo.d.mts +5 -0
  213. package/dist/sensors/useWindowScroll/demo.d.ts +5 -0
  214. package/dist/sensors/useWindowScroll/demo.js +85 -0
  215. package/dist/sensors/useWindowScroll/demo.js.map +1 -0
  216. package/dist/sensors/useWindowScroll/demo.mjs +65 -0
  217. package/dist/sensors/useWindowScroll/demo.mjs.map +1 -0
  218. package/dist/sensors/useWindowScroll/index.d.mts +9 -0
  219. package/dist/sensors/useWindowScroll/index.d.ts +9 -0
  220. package/dist/sensors/useWindowScroll/index.js +36 -0
  221. package/dist/sensors/useWindowScroll/index.js.map +1 -0
  222. package/dist/sensors/useWindowScroll/index.mjs +12 -0
  223. package/dist/sensors/useWindowScroll/index.mjs.map +1 -0
  224. package/dist/shared/configurable.d.mts +21 -0
  225. package/dist/shared/configurable.d.ts +21 -0
  226. package/dist/shared/configurable.js +39 -0
  227. package/dist/shared/configurable.js.map +1 -0
  228. package/dist/shared/configurable.mjs +12 -0
  229. package/dist/shared/configurable.mjs.map +1 -0
  230. package/dist/shared/index.d.mts +4 -0
  231. package/dist/shared/index.d.ts +4 -0
  232. package/dist/shared/index.js +31 -0
  233. package/dist/shared/index.js.map +1 -0
  234. package/dist/shared/index.mjs +7 -0
  235. package/dist/shared/index.mjs.map +1 -0
  236. package/dist/shared/normalizeTargets/index.d.mts +21 -0
  237. package/dist/shared/normalizeTargets/index.d.ts +21 -0
  238. package/dist/shared/normalizeTargets/index.js +36 -0
  239. package/dist/shared/normalizeTargets/index.js.map +1 -0
  240. package/dist/shared/normalizeTargets/index.mjs +12 -0
  241. package/dist/shared/normalizeTargets/index.mjs.map +1 -0
  242. package/dist/shared/utils.d.mts +15 -0
  243. package/dist/shared/utils.d.ts +15 -0
  244. package/dist/shared/utils.js +87 -0
  245. package/dist/shared/utils.js.map +1 -0
  246. package/dist/shared/utils.mjs +52 -0
  247. package/dist/shared/utils.mjs.map +1 -0
  248. package/dist/types.d.mts +52 -0
  249. package/dist/types.d.ts +52 -0
  250. package/dist/types.js +17 -0
  251. package/dist/types.js.map +1 -0
  252. package/dist/types.mjs +1 -0
  253. package/dist/types.mjs.map +1 -0
  254. package/package.json +54 -0
  255. package/src/browser/useEventListener/index.md +109 -0
  256. package/src/browser/useEventListener/index.spec.ts +611 -0
  257. package/src/browser/useEventListener/index.ts +242 -0
  258. package/src/browser/useMediaQuery/demo.tsx +63 -0
  259. package/src/browser/useMediaQuery/index.md +43 -0
  260. package/src/browser/useMediaQuery/index.spec.ts +267 -0
  261. package/src/browser/useMediaQuery/index.ts +96 -0
  262. package/src/components/Auto/index.tsx +65 -0
  263. package/src/elements/useDocumentVisibility/demo.tsx +111 -0
  264. package/src/elements/useDocumentVisibility/index.md +54 -0
  265. package/src/elements/useDocumentVisibility/index.spec.ts +114 -0
  266. package/src/elements/useDocumentVisibility/index.ts +26 -0
  267. package/src/elements/useElementBounding/demo.tsx +68 -0
  268. package/src/elements/useElementBounding/index.md +64 -0
  269. package/src/elements/useElementBounding/index.ts +159 -0
  270. package/src/elements/useElementSize/demo.tsx +53 -0
  271. package/src/elements/useElementSize/index.md +65 -0
  272. package/src/elements/useElementSize/index.spec.ts +295 -0
  273. package/src/elements/useElementSize/index.ts +100 -0
  274. package/src/elements/useElementVisibility/deep-observable-pattern.spec.ts +453 -0
  275. package/src/elements/useElementVisibility/demo.tsx +97 -0
  276. package/src/elements/useElementVisibility/index.md +98 -0
  277. package/src/elements/useElementVisibility/index.spec.ts +227 -0
  278. package/src/elements/useElementVisibility/index.ts +78 -0
  279. package/src/elements/useIntersectionObserver/demo.tsx +180 -0
  280. package/src/elements/useIntersectionObserver/index.md +99 -0
  281. package/src/elements/useIntersectionObserver/index.spec.ts +482 -0
  282. package/src/elements/useIntersectionObserver/index.ts +149 -0
  283. package/src/elements/useMouseInElement/demo.tsx +88 -0
  284. package/src/elements/useMouseInElement/index.md +76 -0
  285. package/src/elements/useMouseInElement/index.spec.ts +398 -0
  286. package/src/elements/useMouseInElement/index.ts +209 -0
  287. package/src/elements/useMutationObserver/demo.tsx +270 -0
  288. package/src/elements/useMutationObserver/index.md +99 -0
  289. package/src/elements/useMutationObserver/index.spec.ts +421 -0
  290. package/src/elements/useMutationObserver/index.ts +66 -0
  291. package/src/elements/useParentElement/demo.tsx +120 -0
  292. package/src/elements/useParentElement/index.md +67 -0
  293. package/src/elements/useParentElement/index.spec.ts +208 -0
  294. package/src/elements/useParentElement/index.ts +35 -0
  295. package/src/elements/useRef$/index.md +62 -0
  296. package/src/elements/useRef$/index.spec.ts +205 -0
  297. package/src/elements/useRef$/index.ts +137 -0
  298. package/src/elements/useRef$/useImperativeHandle.spec.ts +339 -0
  299. package/src/elements/useResizeObserver/demo.tsx +62 -0
  300. package/src/elements/useResizeObserver/index.md +51 -0
  301. package/src/elements/useResizeObserver/index.spec.ts +312 -0
  302. package/src/elements/useResizeObserver/index.ts +106 -0
  303. package/src/elements/useWindowFocus/demo.tsx +79 -0
  304. package/src/elements/useWindowFocus/index.md +38 -0
  305. package/src/elements/useWindowFocus/index.spec.ts +103 -0
  306. package/src/elements/useWindowFocus/index.ts +21 -0
  307. package/src/elements/useWindowSize/demo.tsx +51 -0
  308. package/src/elements/useWindowSize/index.md +55 -0
  309. package/src/elements/useWindowSize/index.spec.ts +310 -0
  310. package/src/elements/useWindowSize/index.ts +107 -0
  311. package/src/function/get/index.md +25 -0
  312. package/src/function/get/index.spec.ts +87 -0
  313. package/src/function/get/index.ts +70 -0
  314. package/src/function/peek/index.spec.ts +97 -0
  315. package/src/function/peek/index.ts +69 -0
  316. package/src/function/useMayObservableOptions/index.spec.ts +521 -0
  317. package/src/function/useMayObservableOptions/index.ts +173 -0
  318. package/src/function/useSupported/index.md +43 -0
  319. package/src/function/useSupported/index.spec.ts +116 -0
  320. package/src/function/useSupported/index.ts +14 -0
  321. package/src/function/useWhenMounted/index.md +25 -0
  322. package/src/function/useWhenMounted/index.spec.ts +120 -0
  323. package/src/function/useWhenMounted/index.ts +16 -0
  324. package/src/index.ts +25 -0
  325. package/src/sensors/useScroll/demo.tsx +103 -0
  326. package/src/sensors/useScroll/index.md +117 -0
  327. package/src/sensors/useScroll/index.spec.ts +678 -0
  328. package/src/sensors/useScroll/index.ts +201 -0
  329. package/src/sensors/useWindowScroll/demo.tsx +78 -0
  330. package/src/sensors/useWindowScroll/index.md +98 -0
  331. package/src/sensors/useWindowScroll/index.spec.ts +69 -0
  332. package/src/sensors/useWindowScroll/index.ts +11 -0
  333. package/src/shared/configurable.ts +35 -0
  334. package/src/shared/index.ts +4 -0
  335. package/src/shared/normalizeTargets/index.spec.ts +76 -0
  336. package/src/shared/normalizeTargets/index.ts +27 -0
  337. package/src/shared/utils.ts +67 -0
  338. package/src/types.ts +56 -0
  339. package/tsconfig.json +9 -0
  340. package/tsup.config.ts +10 -0
  341. package/vitest.config.ts +22 -0
@@ -0,0 +1,242 @@
1
+ "use client";
2
+ import { isObservable, type Observable } from "@legendapp/state";
3
+ import { useObservable, useObserve } from "@legendapp/state/react";
4
+ import { useEffect, useRef } from "react";
5
+ import { isRef$, type MaybeElement } from "../../elements/useRef$";
6
+ import { normalizeTargets } from "../../shared/normalizeTargets";
7
+ import { get } from "../../function/get";
8
+ import type { Arrayable, MaybeObservable } from "../../types";
9
+ import { toArray } from "../../shared/utils";
10
+
11
+ /**
12
+ * Returns true if the value looks like an event name argument (string or
13
+ * array of strings), meaning no explicit target was provided.
14
+ */
15
+ function isEventNameArg(v: unknown): boolean {
16
+ if (typeof v === "string") return true;
17
+ if (Array.isArray(v)) {
18
+ const nonNull = v.filter((item) => item != null);
19
+ return (
20
+ nonNull.length > 0 && nonNull.every((item) => typeof item === "string")
21
+ );
22
+ }
23
+ return false;
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface GeneralEventListener<E = Event> {
31
+ (evt: E): void;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Overloads
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Register using addEventListener on mounted, and removeEventListener
40
+ * automatically on unmounted.
41
+ *
42
+ * Overload 1: Omitted target — defaults to `window`.
43
+ */
44
+ export function useEventListener<E extends keyof WindowEventMap>(
45
+ event: Arrayable<E>,
46
+ listener: Arrayable<(ev: WindowEventMap[E]) => any>,
47
+ options?: MaybeObservable<boolean | AddEventListenerOptions>,
48
+ ): () => void;
49
+
50
+ /**
51
+ * Register using addEventListener on mounted, and removeEventListener
52
+ * automatically on unmounted.
53
+ *
54
+ * Overload 2: Explicit `Window` target.
55
+ */
56
+ export function useEventListener<E extends keyof WindowEventMap>(
57
+ target: Window,
58
+ event: Arrayable<E>,
59
+ listener: Arrayable<(ev: WindowEventMap[E]) => any>,
60
+ options?: MaybeObservable<boolean | AddEventListenerOptions>,
61
+ ): () => void;
62
+
63
+ /**
64
+ * Register using addEventListener on mounted, and removeEventListener
65
+ * automatically on unmounted.
66
+ *
67
+ * Overload 3: Explicit `Document` target.
68
+ */
69
+ export function useEventListener<E extends keyof DocumentEventMap>(
70
+ target: Document,
71
+ event: Arrayable<E>,
72
+ listener: Arrayable<(ev: DocumentEventMap[E]) => any>,
73
+ options?: MaybeObservable<boolean | AddEventListenerOptions>,
74
+ ): () => void;
75
+
76
+ /**
77
+ * Register using addEventListener on mounted, and removeEventListener
78
+ * automatically on unmounted.
79
+ *
80
+ * Overload 4: `MaybeElement` target — supports Ref$, Observable<OpaqueObject<Element>>,
81
+ * Document, Window, or an array of those (Legend-State reactive).
82
+ * Raw HTMLElement is excluded — use Ref$ or Observable<OpaqueObject<Element>> instead.
83
+ */
84
+ export function useEventListener<E extends keyof HTMLElementEventMap>(
85
+ target: MaybeElement | MaybeElement[] | null | undefined,
86
+ event: Arrayable<E>,
87
+ listener: Arrayable<(ev: HTMLElementEventMap[E]) => any>,
88
+ options?: MaybeObservable<boolean | AddEventListenerOptions>,
89
+ ): () => void;
90
+
91
+ /**
92
+ * Register using addEventListener on mounted, and removeEventListener
93
+ * automatically on unmounted.
94
+ *
95
+ * Overload 5: Observable<EventTarget> — reactive target (e.g.
96
+ * Observable<MediaQueryList>, Ref$<HTMLElement>, etc.).
97
+ * The observer re-fires whenever the observable value changes.
98
+ */
99
+ export function useEventListener<EventType = Event>(
100
+ target: Observable<any>,
101
+ event: Arrayable<string>,
102
+ listener: Arrayable<GeneralEventListener<EventType>>,
103
+ options?: MaybeObservable<boolean | AddEventListenerOptions>,
104
+ ): () => void;
105
+
106
+ /**
107
+ * Register using addEventListener on mounted, and removeEventListener
108
+ * automatically on unmounted.
109
+ *
110
+ * Overload 6: Generic `EventTarget` fallback.
111
+ */
112
+ export function useEventListener<EventType = Event>(
113
+ target: EventTarget | null | undefined,
114
+ event: Arrayable<string>,
115
+ listener: Arrayable<GeneralEventListener<EventType>>,
116
+ options?: MaybeObservable<boolean | AddEventListenerOptions>,
117
+ ): () => void;
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Implementation
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export function useEventListener(...args: any[]): () => void {
124
+ // Detect whether first arg is an event name (no target) or a target.
125
+ // Mirrors VueUse's firstParamTargets check.
126
+ const hasTarget = !isEventNameArg(args[0]);
127
+
128
+ const rawTarget: unknown = hasTarget ? args[0] : undefined;
129
+ const rawEvent: Arrayable<string> = hasTarget ? args[1] : args[0];
130
+ const rawListener: Arrayable<(...a: any[]) => any> = hasTarget
131
+ ? args[2]
132
+ : args[1];
133
+ const rawOptions: MaybeObservable<boolean | AddEventListenerOptions> | undefined = hasTarget
134
+ ? args[3]
135
+ : args[2];
136
+
137
+ // Always keep the latest listeners in a ref so that the forwarder always
138
+ // calls the current listeners, even after re-renders change the functions.
139
+ const listenersRef = useRef(toArray(rawListener));
140
+ listenersRef.current = toArray(rawListener);
141
+
142
+ // Stable forwarder — one function reference per hook instance, created once.
143
+ const forwarder = useRef((ev: Event) => {
144
+ listenersRef.current.forEach((l) => l(ev));
145
+ });
146
+
147
+ // Options ref prevents recreating listeners when only options change.
148
+ const optionsRef = useRef(rawOptions);
149
+ optionsRef.current = rawOptions;
150
+
151
+ // Observable mount flag — lets useObserve react when component mounts.
152
+ const mounted$ = useObservable(false);
153
+
154
+ // Array of removeEventListener thunks for the currently active registrations.
155
+ const cleanupsRef = useRef<Array<() => void>>([]);
156
+
157
+ // Single reactive observer: re-runs whenever mounted$ changes OR any
158
+ // observable target changes. Reading .get() inside this callback registers
159
+ // reactive dependencies, so listener setup/teardown is always in sync.
160
+ useObserve(() => {
161
+ // Teardown previous registrations before re-registering.
162
+ cleanupsRef.current.forEach((fn) => fn());
163
+ cleanupsRef.current = [];
164
+
165
+ // Only register listeners while the component is mounted.
166
+ if (!mounted$.get()) return;
167
+
168
+ // Resolve targets inline — reading .get() on observable targets registers
169
+ // them as reactive dependencies for this observer.
170
+ const targets: EventTarget[] = (() => {
171
+ if (!hasTarget) {
172
+ return typeof window !== "undefined" ? [window] : [];
173
+ }
174
+ if (rawTarget == null) return [];
175
+
176
+ const items: unknown[] = Array.isArray(rawTarget)
177
+ ? rawTarget
178
+ : [rawTarget];
179
+ return items.flatMap((item): EventTarget[] => {
180
+ if (item == null) return [];
181
+ // Ref$ references — normalizeTargets handles OpaqueObject.valueOf() unwrapping.
182
+ if (isRef$(item)) {
183
+ return normalizeTargets([item as MaybeElement]) as EventTarget[];
184
+ }
185
+ // Observable targets — .get() unwraps and registers reactive dependency.
186
+ // Supports Element, Document, MediaQueryList, or any EventTarget.
187
+ if (isObservable(item)) {
188
+ const val = (item as { get: () => unknown }).get();
189
+ if (val == null) return [];
190
+ // Unwrap OpaqueObject created by ObservableHint.opaque (has custom valueOf).
191
+ const target =
192
+ typeof (val as any).valueOf === "function"
193
+ ? ((val as any).valueOf() as unknown) ?? val
194
+ : val;
195
+ // Duck-type check: works with real EventTargets and test mocks alike.
196
+ if (typeof (target as any).addEventListener === "function") {
197
+ return [target as EventTarget];
198
+ }
199
+ return [];
200
+ }
201
+ // Raw EventTarget (Window, Document — stable singletons)
202
+ return [item as EventTarget];
203
+ });
204
+ })();
205
+
206
+ if (!targets.length) return;
207
+
208
+ const events = toArray(rawEvent);
209
+ if (!events.length || !listenersRef.current.length) return;
210
+
211
+ const resolvedOpts = get(optionsRef.current);
212
+ const opts =
213
+ typeof resolvedOpts === "object" && resolvedOpts !== null
214
+ ? { ...resolvedOpts }
215
+ : resolvedOpts;
216
+
217
+ const fn = forwarder.current;
218
+ cleanupsRef.current = targets.flatMap((el) =>
219
+ events.map((event) => {
220
+ el.addEventListener(event, fn, opts);
221
+ return () => el.removeEventListener(event, fn, opts);
222
+ }),
223
+ );
224
+ });
225
+
226
+ // useEffect manages mount state only — no setup logic here.
227
+ // eslint-disable-next-line react-hooks/exhaustive-deps
228
+ useEffect(() => {
229
+ mounted$.set(true);
230
+ return () => {
231
+ mounted$.set(false);
232
+ cleanupsRef.current.forEach((fn) => fn());
233
+ cleanupsRef.current = [];
234
+ };
235
+ }, []);
236
+
237
+ // Return a manual cleanup function for imperative removal.
238
+ return () => {
239
+ cleanupsRef.current.forEach((fn) => fn());
240
+ cleanupsRef.current = [];
241
+ };
242
+ }
@@ -0,0 +1,63 @@
1
+ import { useMediaQuery } from ".";
2
+ import { Computed } from "@legendapp/state/react";
3
+
4
+ const row: React.CSSProperties = {
5
+ display: "inline-flex",
6
+ alignItems: "center",
7
+ justifyContent: "start",
8
+ gap: "10px",
9
+ padding: "8px 14px",
10
+ borderRadius: "6px",
11
+ border: "1px solid var(--sl-color-gray-5, #e2e8f0)",
12
+ background: "var(--sl-color-gray-6, #f8fafc)",
13
+ margin: 0,
14
+ };
15
+
16
+ const label: React.CSSProperties = {
17
+ color: "var(--sl-color-gray-3, #64748b)",
18
+ fontSize: "12px",
19
+ };
20
+
21
+ const value: React.CSSProperties = {
22
+ fontFamily: "monospace",
23
+ fontSize: "13px",
24
+ fontWeight: "bold",
25
+ color: "var(--sl-color-text, #0f172a)",
26
+ };
27
+
28
+ export default function UseMediaQueryDemo() {
29
+ const isLargeScreen$ = useMediaQuery("(min-width: 1024px)");
30
+ const prefersDark$ = useMediaQuery("(prefers-color-scheme: dark)");
31
+
32
+ return (
33
+ <div
34
+ style={{
35
+ display: "flex",
36
+ flexDirection: "column",
37
+ gap: "8px",
38
+ fontFamily: "monospace",
39
+ fontSize: "13px",
40
+ }}
41
+ >
42
+ <Computed>
43
+ {() => (
44
+ <div
45
+ style={{
46
+ display: "flex",
47
+ gap: "10px",
48
+ }}
49
+ >
50
+ <div style={row}>
51
+ <span style={label}>isLargeScreen</span>
52
+ <span style={value}>{String(isLargeScreen$.get())}</span>
53
+ </div>
54
+ <div style={row}>
55
+ <span style={label}>prefersDark</span>
56
+ <span style={value}>{String(prefersDark$.get())}</span>
57
+ </div>
58
+ </div>
59
+ )}
60
+ </Computed>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: useMediaQuery
3
+ category: browser
4
+ ---
5
+
6
+ Tracks a CSS media query string as a reactive `Observable<boolean>`.
7
+ Subscribes to `MediaQueryList` change events and updates automatically.
8
+ SSR-safe: accepts an optional `ssrWidth` to statically evaluate `min-width`/`max-width` queries on the server.
9
+
10
+ ## Demo
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { useMediaQuery } from '@usels/core'
16
+ import { Computed } from '@legendapp/state/react'
17
+
18
+ function Component() {
19
+ const isLarge$ = useMediaQuery('(min-width: 1024px)')
20
+
21
+ return (
22
+ <Computed>
23
+ {() => <p>{isLarge$.get() ? 'Large screen' : 'Small screen'}</p>}
24
+ </Computed>
25
+ )
26
+ }
27
+ ```
28
+
29
+ ### Multiple queries
30
+
31
+ ```tsx
32
+ const isLarge$ = useMediaQuery('(min-width: 1024px)')
33
+ const prefersDark$ = useMediaQuery('(prefers-color-scheme: dark)')
34
+ ```
35
+
36
+ ### SSR with ssrWidth
37
+
38
+ ```tsx
39
+ const isLarge$ = useMediaQuery('(min-width: 1024px)', { ssrWidth: 1280 })
40
+ ```
41
+
42
+ `ssrWidth` is used to statically evaluate the query when `window` is unavailable,
43
+ preventing a layout shift between the server-rendered markup and the first client render.
@@ -0,0 +1,267 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { describe, it, expect, vi, afterEach } from "vitest";
4
+ import { observable } from "@legendapp/state";
5
+ import { useMediaQuery, evaluateSSRQuery } from ".";
6
+
7
+ const flush = () => new Promise<void>((resolve) => queueMicrotask(resolve));
8
+ // TODO: change to e2e
9
+ function createMockMatchMedia(initialMatches = false) {
10
+ const listeners = new Map<string, EventListener[]>();
11
+ let currentMatches = initialMatches;
12
+
13
+ const mql = {
14
+ get matches() {
15
+ return currentMatches;
16
+ },
17
+ media: "",
18
+ addEventListener: vi.fn((type: string, listener: EventListener) => {
19
+ if (!listeners.has(type)) listeners.set(type, []);
20
+ listeners.get(type)!.push(listener);
21
+ }),
22
+ removeEventListener: vi.fn((type: string, listener: EventListener) => {
23
+ const list = listeners.get(type) ?? [];
24
+ const idx = list.indexOf(listener);
25
+ if (idx !== -1) list.splice(idx, 1);
26
+ }),
27
+ dispatchEvent: vi.fn((event: Event) => {
28
+ listeners.get(event.type)?.forEach((l) => l(event));
29
+ return true;
30
+ }),
31
+ onchange: null,
32
+ addListener: vi.fn(),
33
+ removeListener: vi.fn(),
34
+ } as unknown as MediaQueryList;
35
+
36
+ const triggerChange = (matches: boolean) => {
37
+ currentMatches = matches;
38
+ const event = {
39
+ type: "change",
40
+ matches,
41
+ } as unknown as MediaQueryListEvent;
42
+ listeners.get("change")?.forEach((l) => l(event));
43
+ };
44
+
45
+ return {
46
+ mockFn: (_query: string) => mql,
47
+ mql,
48
+ triggerChange,
49
+ };
50
+ }
51
+
52
+ afterEach(() => {
53
+ vi.unstubAllGlobals();
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // useMediaQuery — return value
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe("useMediaQuery() — return value", () => {
62
+ it("returns an Observable", () => {
63
+ vi.stubGlobal("matchMedia", createMockMatchMedia().mockFn);
64
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
65
+ expect(typeof result.current.get).toBe("function");
66
+ });
67
+
68
+ it("returns false when matchMedia is not supported", () => {
69
+ const win = globalThis.window as any;
70
+ const original = win.matchMedia;
71
+ delete win.matchMedia;
72
+
73
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
74
+ expect(result.current.get()).toBe(false);
75
+
76
+ win.matchMedia = original;
77
+ });
78
+ });
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // useMediaQuery — client initial value
82
+ // ---------------------------------------------------------------------------
83
+
84
+ describe("useMediaQuery() — client initial value", () => {
85
+ it("initial value is true when matchMedia matches", () => {
86
+ vi.stubGlobal("matchMedia", createMockMatchMedia(true).mockFn);
87
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
88
+ expect(result.current.get()).toBe(true);
89
+ });
90
+
91
+ it("initial value is false when matchMedia does not match", () => {
92
+ vi.stubGlobal("matchMedia", createMockMatchMedia(false).mockFn);
93
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
94
+ expect(result.current.get()).toBe(false);
95
+ });
96
+ });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // useMediaQuery — change event
100
+ // ---------------------------------------------------------------------------
101
+
102
+ describe("useMediaQuery() — change event", () => {
103
+ it("updates to true on change event", () => {
104
+ const { mockFn, triggerChange } = createMockMatchMedia(false);
105
+ vi.stubGlobal("matchMedia", mockFn);
106
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
107
+ expect(result.current.get()).toBe(false);
108
+
109
+ act(() => triggerChange(true));
110
+ expect(result.current.get()).toBe(true);
111
+ });
112
+
113
+ it("updates to false on change event", () => {
114
+ const { mockFn, triggerChange } = createMockMatchMedia(true);
115
+ vi.stubGlobal("matchMedia", mockFn);
116
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
117
+ expect(result.current.get()).toBe(true);
118
+
119
+ act(() => triggerChange(false));
120
+ expect(result.current.get()).toBe(false);
121
+ });
122
+
123
+ it("toggles correctly across multiple change events", () => {
124
+ const { mockFn, triggerChange } = createMockMatchMedia(false);
125
+ vi.stubGlobal("matchMedia", mockFn);
126
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
127
+
128
+ act(() => triggerChange(true));
129
+ expect(result.current.get()).toBe(true);
130
+
131
+ act(() => triggerChange(false));
132
+ expect(result.current.get()).toBe(false);
133
+
134
+ act(() => triggerChange(true));
135
+ expect(result.current.get()).toBe(true);
136
+ });
137
+ });
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // useMediaQuery — SSR (evaluateSSRQuery)
141
+ // ---------------------------------------------------------------------------
142
+
143
+ describe("useMediaQuery() — SSR (evaluateSSRQuery)", () => {
144
+ it("min-width satisfied", () => {
145
+ expect(evaluateSSRQuery("(min-width: 768px)", 1024)).toBe(true);
146
+ });
147
+
148
+ it("min-width not satisfied", () => {
149
+ expect(evaluateSSRQuery("(min-width: 768px)", 500)).toBe(false);
150
+ });
151
+
152
+ it("max-width satisfied", () => {
153
+ expect(evaluateSSRQuery("(max-width: 768px)", 500)).toBe(true);
154
+ });
155
+
156
+ it("max-width not satisfied", () => {
157
+ expect(evaluateSSRQuery("(max-width: 768px)", 1024)).toBe(false);
158
+ });
159
+
160
+ it("not all negation", () => {
161
+ expect(evaluateSSRQuery("not all and (min-width: 768px)", 500)).toBe(true);
162
+ });
163
+
164
+ it("comma-separated queries match when one condition is met", () => {
165
+ expect(
166
+ evaluateSSRQuery("(max-width: 500px), (min-width: 768px)", 1024),
167
+ ).toBe(true);
168
+ });
169
+
170
+ it("defaults to false when no ssrWidth is provided", () => {
171
+ const win = globalThis.window as any;
172
+ const original = win.matchMedia;
173
+ delete win.matchMedia;
174
+
175
+ const { result } = renderHook(() => useMediaQuery("(min-width: 768px)"));
176
+ expect(result.current.get()).toBe(false);
177
+
178
+ win.matchMedia = original;
179
+ });
180
+
181
+ it("query without min/max-width resolves to false", () => {
182
+ expect(evaluateSSRQuery("(prefers-color-scheme: dark)", 1024)).toBe(false);
183
+ });
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // useMediaQuery — cleanup
188
+ // ---------------------------------------------------------------------------
189
+
190
+ describe("useMediaQuery() — cleanup", () => {
191
+ it("removes change listener on unmount", async () => {
192
+ const { mockFn, mql } = createMockMatchMedia(false);
193
+ vi.stubGlobal("matchMedia", mockFn);
194
+
195
+ const { unmount } = renderHook(() => useMediaQuery("(min-width: 768px)"));
196
+ unmount();
197
+ await flush();
198
+
199
+ expect(mql.removeEventListener).toHaveBeenCalledWith(
200
+ "change",
201
+ expect.any(Function),
202
+ expect.objectContaining({ passive: true }),
203
+ );
204
+ });
205
+
206
+ it("does not update after unmount", async () => {
207
+ const { mockFn, triggerChange } = createMockMatchMedia(false);
208
+ vi.stubGlobal("matchMedia", mockFn);
209
+
210
+ const { result, unmount } = renderHook(() =>
211
+ useMediaQuery("(min-width: 768px)"),
212
+ );
213
+ unmount();
214
+ await flush();
215
+
216
+ act(() => triggerChange(true));
217
+ expect(result.current.get()).toBe(false);
218
+ });
219
+ });
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // useMediaQuery — reactive query (MaybeObservable<string>)
223
+ // ---------------------------------------------------------------------------
224
+
225
+ describe("useMediaQuery() — reactive query (MaybeObservable<string>)", () => {
226
+ it("accepts Observable<string> and reads initial value", () => {
227
+ vi.stubGlobal("matchMedia", createMockMatchMedia(true).mockFn);
228
+ const query$ = observable("(min-width: 768px)");
229
+
230
+ const { result } = renderHook(() => useMediaQuery(query$));
231
+ expect(result.current.get()).toBe(true);
232
+ });
233
+
234
+ it("updates matches when query Observable changes", () => {
235
+ const mqlMap = new Map<string, ReturnType<typeof createMockMatchMedia>>();
236
+ vi.stubGlobal("matchMedia", (q: string) => {
237
+ if (!mqlMap.has(q))
238
+ mqlMap.set(q, createMockMatchMedia(q === "(max-width: 480px)"));
239
+ return mqlMap.get(q)!.mql;
240
+ });
241
+
242
+ const query$ = observable("(min-width: 1024px)");
243
+ const { result } = renderHook(() => useMediaQuery(query$));
244
+ expect(result.current.get()).toBe(false);
245
+
246
+ act(() => query$.set("(max-width: 480px)"));
247
+ expect(result.current.get()).toBe(true);
248
+ });
249
+
250
+ it("removes old change listener and adds new one when query changes", () => {
251
+ const mqlMap = new Map<string, ReturnType<typeof createMockMatchMedia>>();
252
+ vi.stubGlobal("matchMedia", (q: string) => {
253
+ if (!mqlMap.has(q)) mqlMap.set(q, createMockMatchMedia(false));
254
+ return mqlMap.get(q)!.mql;
255
+ });
256
+
257
+ const query$ = observable("(min-width: 1024px)");
258
+ renderHook(() => useMediaQuery(query$));
259
+
260
+ act(() => query$.set("(max-width: 480px)"));
261
+
262
+ expect(mqlMap.get("(min-width: 1024px)")!.mql.removeEventListener)
263
+ .toHaveBeenCalledWith("change", expect.any(Function), expect.objectContaining({ passive: true }));
264
+ expect(mqlMap.get("(max-width: 480px)")!.mql.addEventListener)
265
+ .toHaveBeenCalledWith("change", expect.any(Function), { passive: true });
266
+ });
267
+ });