@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,173 @@
1
+ import {
2
+ isObservable,
3
+ ObservableHint,
4
+ type Observable,
5
+ } from "@legendapp/state";
6
+ import { useObservable } from "@legendapp/state/react";
7
+ import { useMemo, useRef } from "react";
8
+ import { get } from "../get";
9
+ import { peek } from "../peek";
10
+ import type { DeepMaybeObservable, MaybeObservable } from "../../types";
11
+ import { getElement, peekElement } from "../../elements/useRef$";
12
+ import type { MaybeElement } from "../../elements/useRef$";
13
+
14
+ /**
15
+ * Per-field resolution hint for the object-form transform.
16
+ *
17
+ * Resolution axis (`get` / `peek`):
18
+ * - `'get'` — no-op; Legend-State auto-derefs the field Observable and registers the dep at the call site. **Default.**
19
+ * - `'peek'` — `peek(fieldValue)` — no dep, mount-time-only snapshot.
20
+ *
21
+ * Legend-State wrapping axis (dot-notation):
22
+ * - `'get.opaque'` — `get()` then `ObservableHint.opaque()`. Null-safe.
23
+ * - `'get.plain'` — `get()` then `ObservableHint.plain()`. Prevents nested auto-deref. Null-safe.
24
+ * - `'get.function'` — `get()` then `ObservableHint.function()`. For callbacks. Null-safe.
25
+ * - `'get.element'` — `getElement(fieldValue)` (reactive) then `ObservableHint.opaque()`. For MaybeElement.
26
+ * - `'peek.element'` — `peekElement(fieldValue)` (non-reactive) then `ObservableHint.opaque()`. For MaybeElement.
27
+ *
28
+ * Escape hatch:
29
+ * - `(value) => R` — custom transform function.
30
+ */
31
+ export type FieldHint<V = any, R = any> =
32
+ | "get"
33
+ | "peek"
34
+ | "get.opaque"
35
+ | "get.plain"
36
+ | "get.function"
37
+ | "get.element"
38
+ | "peek.element"
39
+ | ((value: MaybeObservable<V>) => R);
40
+
41
+ /**
42
+ * Maps each field of `T` to a `FieldHint`.
43
+ * Fields not specified default to `'get'` (reactive resolution via Legend-State auto-deref).
44
+ */
45
+ export type FieldTransformMap<T> = {
46
+ [K in keyof T]?: FieldHint<T[K], any>;
47
+ };
48
+
49
+ /**
50
+ * The `transform` parameter for `useMayObservableOptions`.
51
+ * - **Object form:** declarative per-field hints — `FieldTransformMap<T>`.
52
+ * - **Function form:** full custom compute — `(current) => T | undefined`.
53
+ */
54
+ export type Transform<T> =
55
+ | FieldTransformMap<T>
56
+ | ((current: DeepMaybeObservable<T> | undefined) => T | undefined);
57
+
58
+ function applyObjectTransform<T>(
59
+ raw: T | undefined,
60
+ map: FieldTransformMap<T>,
61
+ ): T | undefined {
62
+ if (raw == null) return undefined;
63
+ const result = { ...raw } as Record<string, unknown>;
64
+ for (const key of Object.keys(result)) {
65
+ const hint = (map as Record<string, FieldHint>)[key];
66
+ const fieldValue = result[key] as MaybeObservable<unknown>;
67
+ switch (hint) {
68
+ case "peek":
69
+ result[key] = peek(fieldValue);
70
+ break;
71
+ case "get.opaque": {
72
+ const v = get(fieldValue);
73
+ if (v != null) result[key] = ObservableHint.opaque(v);
74
+ break;
75
+ }
76
+ case "get.plain": {
77
+ const v = get(fieldValue);
78
+ if (v != null) result[key] = ObservableHint.plain(v as object);
79
+ break;
80
+ }
81
+ case "get.function": {
82
+ const v = get(fieldValue);
83
+ if (v != null)
84
+ result[key] = ObservableHint.function(
85
+ v as (...args: unknown[]) => unknown,
86
+ );
87
+ break;
88
+ }
89
+ case "get.element": {
90
+ if (fieldValue !== undefined) {
91
+ // Observable resolving to undefined means "not set" — propagate undefined
92
+ if (isObservable(fieldValue) && (fieldValue as Observable<unknown>).get() === undefined) {
93
+ result[key] = undefined;
94
+ break;
95
+ }
96
+ const el = getElement(fieldValue as MaybeElement);
97
+ result[key] = el != null ? ObservableHint.opaque(el) : null;
98
+ }
99
+ break;
100
+ }
101
+ case "peek.element": {
102
+ if (fieldValue !== undefined) {
103
+ // Observable resolving to undefined means "not set" — propagate undefined
104
+ if (isObservable(fieldValue) && (fieldValue as Observable<unknown>).peek() === undefined) {
105
+ result[key] = undefined;
106
+ break;
107
+ }
108
+ const el = peekElement(fieldValue as MaybeElement);
109
+ result[key] = el != null ? ObservableHint.opaque(el) : null;
110
+ }
111
+ break;
112
+ }
113
+ default:
114
+ if (typeof hint === "function") {
115
+ result[key] = hint(fieldValue);
116
+ }
117
+ // 'get' or undefined → no action; Legend-State auto-derefs per-field Observables
118
+ }
119
+ }
120
+ return result as T;
121
+ }
122
+
123
+ /**
124
+ * Normalizes `DeepMaybeObservable<T>` into a stable computed `Observable<T | undefined>`.
125
+ *
126
+ * Handles three cases without interference:
127
+ * - **Outer `Observable<T>`** — tracked via `.get()` dep inside the compute fn
128
+ * - **Per-field `{ field: Observable<T[K]> }`** — tracked via explicit `get()` per field
129
+ * - **Plain value changing between renders** — tracked via Symbol depKey (React-level)
130
+ *
131
+ * The Symbol depKey prevents Legend-State from auto-deref'ing inner Observables
132
+ * inside `depsObs$`, which would break per-field dep tracking if the raw `options`
133
+ * object were passed directly as the dep array item.
134
+ *
135
+ * @param options - DeepMaybeObservable options to normalize
136
+ * @param transform - Optional transform. Two forms:
137
+ * - **Object form:** `FieldTransformMap<T>` — per-field hints (`'peek'`, `'get.opaque'`, etc.).
138
+ * Defaults to `'get'` for unspecified fields.
139
+ * **Note:** has no effect when `options` is an outer `Observable<T>` — in that case the
140
+ * proxy is returned as-is (same as no transform), preserving Legend-State's
141
+ * reference-equality tracking behavior. Use per-field Observables or plain objects
142
+ * when field-level hints are needed.
143
+ * - **Function form:** `(current) => T | undefined` — full custom compute for complex cases.
144
+ */
145
+ export function useMayObservableOptions<T>(
146
+ options: DeepMaybeObservable<T> | undefined,
147
+ transform?: Transform<T>,
148
+ ): Observable<T | undefined> {
149
+ const optionsRef = useRef(options);
150
+ optionsRef.current = options;
151
+ // eslint-disable-next-line react-hooks/exhaustive-deps
152
+ const depKey = useMemo(() => Symbol(), [options]);
153
+ const compute = (): T | undefined => {
154
+ if (typeof transform === "function") {
155
+ return transform(optionsRef.current);
156
+ }
157
+ const raw = optionsRef.current;
158
+ const resolved = get(raw as MaybeObservable<T> | undefined);
159
+ if (transform != null && !isObservable(raw)) {
160
+ // Object form — only applies when options is NOT an outer Observable.
161
+ // For outer Observable, returning the proxy as-is preserves the
162
+ // "reference-equality tracking" behavior: child-field mutations do not
163
+ // trigger opts$ recomputation (documented known Legend-State limitation).
164
+ // For per-field Observable or plain object, apply field hints.
165
+ return applyObjectTransform(resolved, transform);
166
+ }
167
+ return resolved;
168
+ };
169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
170
+ return useObservable(compute, [depKey]) as unknown as Observable<
171
+ T | undefined
172
+ >;
173
+ }
@@ -0,0 +1,43 @@
1
+ ---
2
+ title: useSupported
3
+ description: SSR-safe browser feature detection as a reactive Observable
4
+ category: Browser Utilities
5
+ ---
6
+
7
+ Check whether a browser API or feature is supported, returning a reactive `Observable<boolean>` that re-evaluates after the component mounts. Safe to use during SSR — the value is always `false` on the server and updates on the client after hydration.
8
+
9
+ ## Usage
10
+
11
+ ```typescript
12
+ import { useSupported } from '@usels/core'
13
+
14
+ const isMatchMediaSupported = useSupported(() => 'matchMedia' in window)
15
+
16
+ // Use the observable value
17
+ if (isMatchMediaSupported.get()) {
18
+ // matchMedia is available
19
+ }
20
+ ```
21
+
22
+ ### With Legend-State reactive bindings
23
+
24
+ ```typescript
25
+ import { useSupported } from '@usels/core'
26
+ import { Memo } from '@legendapp/state/react'
27
+
28
+ const isIntersectionObserverSupported = useSupported(
29
+ () => 'IntersectionObserver' in window
30
+ )
31
+
32
+ function MyComponent() {
33
+ return (
34
+ <Memo>
35
+ {() =>
36
+ isIntersectionObserverSupported.get()
37
+ ? <FeatureComponent />
38
+ : <Fallback />
39
+ }
40
+ </Memo>
41
+ )
42
+ }
43
+ ```
@@ -0,0 +1,116 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { observable } from "@legendapp/state";
4
+ import { describe, it, expect, vi, afterEach } from "vitest";
5
+ import { useSupported } from ".";
6
+
7
+ afterEach(() => {
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // useSupported — return value
13
+ // ---------------------------------------------------------------------------
14
+
15
+ describe("useSupported() — return value", () => {
16
+ it("returns an Observable", () => {
17
+ const { result } = renderHook(() => useSupported(() => true));
18
+ expect(typeof result.current.get).toBe("function");
19
+ });
20
+
21
+ it("returns true when callback returns a truthy value", () => {
22
+ const { result } = renderHook(() => useSupported(() => true));
23
+ expect(result.current.get()).toBe(true);
24
+ });
25
+
26
+ it("returns false when callback returns a falsy value", () => {
27
+ const { result } = renderHook(() => useSupported(() => false));
28
+ expect(result.current.get()).toBe(false);
29
+ });
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // useSupported — Boolean coercion
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("useSupported() — Boolean coercion", () => {
37
+ it("coerces a non-empty string to true", () => {
38
+ const { result } = renderHook(() => useSupported(() => "supported"));
39
+ expect(result.current.get()).toBe(true);
40
+ });
41
+
42
+ it("coerces an object to true", () => {
43
+ const { result } = renderHook(() => useSupported(() => ({})));
44
+ expect(result.current.get()).toBe(true);
45
+ });
46
+
47
+ it("coerces 1 to true", () => {
48
+ const { result } = renderHook(() => useSupported(() => 1));
49
+ expect(result.current.get()).toBe(true);
50
+ });
51
+
52
+ it("coerces 0 to false", () => {
53
+ const { result } = renderHook(() => useSupported(() => 0));
54
+ expect(result.current.get()).toBe(false);
55
+ });
56
+
57
+ it("coerces null to false", () => {
58
+ const { result } = renderHook(() => useSupported(() => null));
59
+ expect(result.current.get()).toBe(false);
60
+ });
61
+
62
+ it("coerces undefined to false", () => {
63
+ const { result } = renderHook(() => useSupported(() => undefined));
64
+ expect(result.current.get()).toBe(false);
65
+ });
66
+
67
+ it("coerces empty string to false", () => {
68
+ const { result } = renderHook(() => useSupported(() => ""));
69
+ expect(result.current.get()).toBe(false);
70
+ });
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // useSupported — mount re-evaluation
75
+ // ---------------------------------------------------------------------------
76
+
77
+ describe("useSupported() — mount re-evaluation", () => {
78
+ it("invokes the callback when the observable value is accessed", () => {
79
+ // useObservable with a selector is lazy — callback runs on .get(), not at creation
80
+ const callback = vi.fn(() => true);
81
+ const { result } = renderHook(() => useSupported(callback));
82
+ result.current.get();
83
+ expect(callback).toHaveBeenCalled();
84
+ });
85
+
86
+ it("re-evaluates when a reactive dependency inside the callback changes", () => {
87
+ // callback reads an observable — Legend State tracks it as a dependency
88
+ const flag$ = observable(false);
89
+ const { result } = renderHook(() => useSupported(() => flag$.get()));
90
+
91
+ expect(result.current.get()).toBe(false);
92
+
93
+ act(() => flag$.set(true));
94
+ expect(result.current.get()).toBe(true);
95
+ });
96
+
97
+ it("reflects a browser API check after mount", () => {
98
+ const { result } = renderHook(() =>
99
+ useSupported(() => "matchMedia" in window),
100
+ );
101
+ expect(result.current.get()).toBe(true);
102
+ });
103
+
104
+ it("returns false for an unavailable browser API after mount", () => {
105
+ const win = globalThis.window as any;
106
+ const original = win.matchMedia;
107
+ delete win.matchMedia;
108
+
109
+ const { result } = renderHook(() =>
110
+ useSupported(() => "matchMedia" in window),
111
+ );
112
+ expect(result.current.get()).toBe(false);
113
+
114
+ win.matchMedia = original;
115
+ });
116
+ });
@@ -0,0 +1,14 @@
1
+ import { Observable } from "@legendapp/state";
2
+ import { useIsMounted, useObservable } from "@legendapp/state/react";
3
+
4
+ export type UseSupportedReturn = Observable<boolean>;
5
+
6
+ /* @__NO_SIDE_EFFECTS__ */
7
+ export function useSupported(callback: () => unknown): UseSupportedReturn {
8
+ const isMounted = useIsMounted();
9
+
10
+ return useObservable(() => {
11
+ if (!isMounted.get()) return false;
12
+ return Boolean(callback());
13
+ });
14
+ }
@@ -0,0 +1,25 @@
1
+ ---
2
+ title: useWhenMounted
3
+ category: Browser Utilities
4
+ ---
5
+
6
+ Execute a callback and expose its return value as a reactive `Observable<T | undefined>` — only after the component has mounted. Returns `undefined` during SSR and before hydration, then re-evaluates with the actual callback value once mounted. Unlike `useSupported`, the return value is not coerced to boolean — the full type `T` is preserved.
7
+
8
+ ## Usage
9
+
10
+ ```typescript
11
+ import { useWhenMounted } from '@usels/core'
12
+
13
+ const windowWidth = useWhenMounted(() => window.innerWidth)
14
+
15
+ // undefined on the server, actual width after mount
16
+ console.log(windowWidth.get())
17
+ ```
18
+
19
+ ### Deferred browser API access
20
+
21
+ ```typescript
22
+ import { useWhenMounted } from '@usels/core'
23
+
24
+ const scrollY = useWhenMounted(() => window.scrollY)
25
+ ```
@@ -0,0 +1,120 @@
1
+ // @vitest-environment jsdom
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { observable } from "@legendapp/state";
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import { useWhenMounted } from ".";
6
+
7
+ // Shared observable that controls the mocked isMounted state.
8
+ // Initialized in beforeEach so each test starts with a fresh observable.
9
+ let isMounted$: ReturnType<typeof observable<boolean>>;
10
+
11
+ vi.mock("@legendapp/state/react", async () => {
12
+ const actual = await import("@legendapp/state/react");
13
+ return {
14
+ ...actual,
15
+ // Return the shared observable lazily — evaluated at call time, not setup time
16
+ useIsMounted: () => isMounted$,
17
+ };
18
+ });
19
+
20
+ beforeEach(() => {
21
+ isMounted$ = observable(true); // default: mounted
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ });
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // useWhenMounted — return value
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe("useWhenMounted() — return value", () => {
33
+ it("returns an Observable", () => {
34
+ const { result } = renderHook(() => useWhenMounted(() => "hello"));
35
+ expect(typeof result.current.get).toBe("function");
36
+ });
37
+
38
+ it("returns the callback value when mounted", () => {
39
+ const { result } = renderHook(() => useWhenMounted(() => "hello"));
40
+ expect(result.current.get()).toBe("hello");
41
+ });
42
+
43
+ it("returns undefined when not mounted", () => {
44
+ isMounted$.set(false);
45
+ const { result } = renderHook(() => useWhenMounted(() => "hello"));
46
+ expect(result.current.get()).toBeUndefined();
47
+ });
48
+ });
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // useWhenMounted — type preservation (no boolean coercion)
52
+ // ---------------------------------------------------------------------------
53
+
54
+ describe("useWhenMounted() — type preservation", () => {
55
+ it("returns false as-is (no boolean coercion)", () => {
56
+ const { result } = renderHook(() => useWhenMounted(() => false));
57
+ expect(result.current.get()).toBe(false);
58
+ });
59
+
60
+ it("returns 0 as-is", () => {
61
+ const { result } = renderHook(() => useWhenMounted(() => 0));
62
+ expect(result.current.get()).toBe(0);
63
+ });
64
+
65
+ it("returns a string as-is", () => {
66
+ const { result } = renderHook(() => useWhenMounted(() => "value"));
67
+ expect(result.current.get()).toBe("value");
68
+ });
69
+
70
+ it("returns a number as-is", () => {
71
+ const { result } = renderHook(() => useWhenMounted(() => 42));
72
+ expect(result.current.get()).toBe(42);
73
+ });
74
+
75
+ it("returns an object reference", () => {
76
+ const obj = { foo: "bar" };
77
+ const { result } = renderHook(() => useWhenMounted(() => obj));
78
+ expect(result.current.get()).toEqual({ foo: "bar" });
79
+ });
80
+
81
+ // Legend State observables treat null as an empty/deleted value and normalize it to undefined
82
+ it("normalizes null to undefined (Legend State behavior)", () => {
83
+ const { result } = renderHook(() => useWhenMounted(() => null));
84
+ expect(result.current.get()).toBeUndefined();
85
+ });
86
+ });
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // useWhenMounted — reactive re-evaluation
90
+ // ---------------------------------------------------------------------------
91
+
92
+ describe("useWhenMounted() — reactive re-evaluation", () => {
93
+ it("invokes the callback when the observable value is accessed", () => {
94
+ const callback = vi.fn(() => "value");
95
+ const { result } = renderHook(() => useWhenMounted(callback));
96
+ result.current.get();
97
+ expect(callback).toHaveBeenCalled();
98
+ });
99
+
100
+ it("re-evaluates when a reactive dependency inside the callback changes", () => {
101
+ const count$ = observable(1);
102
+ const { result } = renderHook(() =>
103
+ useWhenMounted(() => count$.get() * 2),
104
+ );
105
+
106
+ expect(result.current.get()).toBe(2);
107
+
108
+ act(() => count$.set(5));
109
+ expect(result.current.get()).toBe(10);
110
+ });
111
+
112
+ it("transitions from undefined to callback value when isMounted becomes true", () => {
113
+ isMounted$.set(false);
114
+ const { result } = renderHook(() => useWhenMounted(() => "ready"));
115
+ expect(result.current.get()).toBeUndefined();
116
+
117
+ act(() => isMounted$.set(true));
118
+ expect(result.current.get()).toBe("ready");
119
+ });
120
+ });
@@ -0,0 +1,16 @@
1
+ import type { Observable } from "@legendapp/state";
2
+ import { useIsMounted, useObservable } from "@legendapp/state/react";
3
+
4
+ export type UseWhenMountedReturn<T> = Observable<T | undefined>;
5
+
6
+ /* @__NO_SIDE_EFFECTS__ */
7
+ export function useWhenMounted<T>(
8
+ callback: () => T,
9
+ ): UseWhenMountedReturn<T> {
10
+ const isMounted = useIsMounted();
11
+
12
+ return useObservable(() => {
13
+ if (!isMounted.get()) return undefined;
14
+ return callback();
15
+ }) as unknown as Observable<T | undefined>;
16
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ export * from "./types";
2
+
3
+ export * from "./function/get";
4
+ export * from "./function/peek";
5
+ export * from "./function/useSupported";
6
+ export * from "./function/useWhenMounted";
7
+
8
+ export * from "./elements/useRef$";
9
+ export * from "./elements/useResizeObserver";
10
+ export * from "./elements/useElementSize";
11
+ export * from "./elements/useElementBounding";
12
+ export * from "./elements/useMutationObserver";
13
+ export * from "./elements/useIntersectionObserver";
14
+ export * from "./elements/useElementVisibility";
15
+ export * from "./elements/useDocumentVisibility";
16
+ export * from "./elements/useWindowFocus";
17
+ export * from "./elements/useWindowSize";
18
+ export * from "./elements/useParentElement";
19
+ export * from "./elements/useMouseInElement";
20
+
21
+ export * from "./sensors/useScroll";
22
+ export * from "./sensors/useWindowScroll";
23
+
24
+ export * from "./browser/useEventListener";
25
+ export * from "./browser/useMediaQuery";
@@ -0,0 +1,103 @@
1
+ import { Computed } from "@legendapp/state/react";
2
+ import { useRef$ } from "../../elements/useRef$";
3
+ import { useScroll } from ".";
4
+
5
+ const badge = (active: boolean): React.CSSProperties => ({
6
+ display: "inline-flex",
7
+ alignItems: "center",
8
+ padding: "2px 8px",
9
+ borderRadius: "4px",
10
+ fontSize: "12px",
11
+ fontFamily: "monospace",
12
+ background: active
13
+ ? "var(--sl-color-green-low, #f0fdf4)"
14
+ : "var(--sl-color-gray-5, #f1f5f9)",
15
+ color: active
16
+ ? "var(--sl-color-green, #16a34a)"
17
+ : "var(--sl-color-gray-3, #64748b)",
18
+ border: `1px solid ${active ? "var(--sl-color-green, #22c55e)" : "var(--sl-color-gray-4, #e2e8f0)"}`,
19
+ transition: "all 0.15s",
20
+ });
21
+
22
+ export default function UseScrollDemo() {
23
+ const el$ = useRef$<HTMLDivElement>();
24
+ const { x, y, isScrolling, arrivedState, directions } = useScroll(el$);
25
+
26
+ return (
27
+ <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
28
+ {/* Stats */}
29
+ <Computed>
30
+ {() => (
31
+ <div
32
+ style={{
33
+ display: "flex",
34
+ flexWrap: "wrap",
35
+ gap: "8px",
36
+ padding: "8px 12px",
37
+ borderRadius: "6px",
38
+ border: "1px solid var(--sl-color-gray-5, #e2e8f0)",
39
+ background: "var(--sl-color-gray-6, #f8fafc)",
40
+ fontFamily: "monospace",
41
+ fontSize: "13px",
42
+ }}
43
+ >
44
+ <span>
45
+ x: <strong>{x.get()}</strong>
46
+ </span>
47
+ <span>
48
+ y: <strong>{y.get()}</strong>
49
+ </span>
50
+ <span style={badge(isScrolling.get())}>
51
+ {isScrolling.get() ? "scrolling" : "idle"}
52
+ </span>
53
+ <span style={badge(arrivedState.top.get())}>top</span>
54
+ <span style={badge(arrivedState.bottom.get())}>bottom</span>
55
+ <span style={{ color: "var(--sl-color-gray-3, #94a3b8)" }}>
56
+ {directions.top.get() && "↑"}
57
+ {directions.bottom.get() && "↓"}
58
+ {directions.left.get() && "←"}
59
+ {directions.right.get() && "→"}
60
+ </span>
61
+ </div>
62
+ )}
63
+ </Computed>
64
+
65
+ {/* Scrollable container */}
66
+ <div
67
+ ref={el$}
68
+ style={{
69
+ height: "200px",
70
+ overflowY: "auto",
71
+ borderRadius: "6px",
72
+ border: "1px solid var(--sl-color-gray-5, #e2e8f0)",
73
+ background: "var(--sl-color-gray-7, #fff)",
74
+ }}
75
+ >
76
+ {Array.from({ length: 20 }, (_, i) => (
77
+ <div
78
+ key={i}
79
+ style={{
80
+ padding: "8px 14px",
81
+ borderBottom: "1px solid var(--sl-color-gray-6, #f1f5f9)",
82
+ fontSize: "13px",
83
+ color: "var(--sl-color-gray-2, #475569)",
84
+ fontFamily: "monospace",
85
+ }}
86
+ >
87
+ Item {i + 1}
88
+ </div>
89
+ ))}
90
+ </div>
91
+
92
+ <p
93
+ style={{
94
+ margin: 0,
95
+ fontSize: "11px",
96
+ color: "var(--sl-color-gray-3, #94a3b8)",
97
+ }}
98
+ >
99
+ Scroll inside the box to see x, y, arrivedState, and directions update.
100
+ </p>
101
+ </div>
102
+ );
103
+ }