@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,453 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Experiment: useObservable(() => get(options)) pattern for DeepMaybeObservable
4
+ *
5
+ * Verified behaviors:
6
+ *
7
+ * Case 1 — outer Observable<T>
8
+ * useObservable(() => get(options$)) reactively tracks options$ changes ✓
9
+ * Dep registered via options$.get() inside reactive context.
10
+ *
11
+ * Case 2 — per-field { field: Observable<T[K]> }
12
+ * Legend-State auto-dereferences inner Observables — no double-nesting ✓
13
+ * computed$.field.get() returns the plain value (e.g. "0px"), NOT Observable<T>
14
+ * Inner Observable changes ARE reflected via field-level dep tracking (not callback re-eval) ✓
15
+ * The outer useObservable callback is NOT re-evaluated on inner field changes.
16
+ *
17
+ * Case 3 — useElementVisibility with Element (scrollTarget) options
18
+ * Per-field scrollTarget as Ref$, Observable<HTMLElement | null>(null), plain HTMLElement
19
+ * Note: Observable<HTMLElement> starting from non-null is NOT reliably tracked
20
+ * (Legend-State deeply proxies HTMLElements; use Ref$ or start from null).
21
+ */
22
+ import { act, renderHook } from "@testing-library/react";
23
+ import { isObservable, observable, ObservableHint } from "@legendapp/state";
24
+ import type { OpaqueObject } from "@legendapp/state";
25
+ import { useObservable } from "@legendapp/state/react";
26
+
27
+ const wrapEl = (el: Element) => observable<OpaqueObject<Element> | null>(ObservableHint.opaque(el));
28
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
29
+ import { get } from "../../function/get";
30
+ import { useRef$ } from "../useRef$";
31
+ import { useElementVisibility } from ".";
32
+ import { useIntersectionObserver } from "../useIntersectionObserver";
33
+
34
+ // --- IntersectionObserver mock ---
35
+
36
+ const mockObserve = vi.fn();
37
+ const mockDisconnect = vi.fn();
38
+ let capturedInit: IntersectionObserverInit | undefined;
39
+
40
+ const MockIntersectionObserver = vi.fn(
41
+ (_cb: IntersectionObserverCallback, init?: IntersectionObserverInit) => {
42
+ capturedInit = init;
43
+ return { observe: mockObserve, disconnect: mockDisconnect };
44
+ },
45
+ );
46
+
47
+ beforeEach(() => {
48
+ vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
49
+ mockObserve.mockClear();
50
+ mockDisconnect.mockClear();
51
+ MockIntersectionObserver.mockClear();
52
+ capturedInit = undefined;
53
+ });
54
+
55
+ afterEach(() => {
56
+ vi.unstubAllGlobals();
57
+ });
58
+
59
+ // =============================================================================
60
+ // Case 1: outer Observable<T>
61
+ // =============================================================================
62
+
63
+ describe("Case 1 — outer Observable<T>: useObservable(() => get(options$))", () => {
64
+ it("computed$ reflects updated rootMargin when outer Observable changes", () => {
65
+ const options$ = observable({ rootMargin: "0px" });
66
+
67
+ const { result } = renderHook(() => {
68
+ // useObservable(fn) — Legend-State tracks .get() calls inside fn
69
+ const computed$ = useObservable(() => get(options$));
70
+ return { computed$ };
71
+ });
72
+
73
+ expect(result.current.computed$.rootMargin.get()).toBe("0px");
74
+
75
+ act(() => {
76
+ options$.rootMargin.set("20px");
77
+ });
78
+
79
+ // options$.get() was called in reactive context → dep registered
80
+ // → computed$ recomputes when options$ changes
81
+ expect(result.current.computed$.rootMargin.get()).toBe("20px");
82
+ });
83
+
84
+ it("computed$ reflects all field changes in outer Observable", () => {
85
+ const options$ = observable({ rootMargin: "0px", threshold: 0 });
86
+
87
+ const { result } = renderHook(() => {
88
+ const computed$ = useObservable(() => get(options$));
89
+ return { computed$ };
90
+ });
91
+
92
+ act(() => {
93
+ options$.set({ rootMargin: "10px", threshold: 0.5 });
94
+ });
95
+
96
+ expect(result.current.computed$.rootMargin.get()).toBe("10px");
97
+ expect(result.current.computed$.threshold.get()).toBe(0.5);
98
+ });
99
+ });
100
+
101
+ // =============================================================================
102
+ // Case 2: per-field { field: Observable<T[K]> } — auto-dereference behavior
103
+ // =============================================================================
104
+
105
+ describe("Case 2 — per-field { field: obs$ }: Legend-State auto-dereferences inner Observables", () => {
106
+ it("computed$.rootMargin.get() returns plain string — no double-nesting", () => {
107
+ const rootMargin$ = observable("0px");
108
+
109
+ const { result } = renderHook(() => {
110
+ // get({ rootMargin: rootMargin$ }) returns the plain object as-is (not Observable)
111
+ // useObservable wraps it, but Legend-State auto-dereferences inner Observables
112
+ // → computed$.rootMargin.get() returns "0px" (string), NOT Observable<string>
113
+ const computed$ = useObservable(() =>
114
+ get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ }),
115
+ );
116
+ return { computed$ };
117
+ });
118
+
119
+ const fieldValue = result.current.computed$.rootMargin.get();
120
+ // Legend-State auto-dereferences: returns plain string, not Observable
121
+ expect(isObservable(fieldValue)).toBe(false);
122
+ expect(fieldValue).toBe("0px");
123
+ });
124
+
125
+ it("changing inner Observable does NOT re-evaluate computed$ (no dep registered)", () => {
126
+ const rootMargin$ = observable("0px");
127
+ let evalCount = 0;
128
+
129
+ renderHook(() => {
130
+ useObservable(() => {
131
+ evalCount++;
132
+ // get() on plain object: isObservable = false → returns as-is, no .get() called
133
+ // → rootMargin$ is never called .get() → dep NOT registered
134
+ return get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ });
135
+ });
136
+ });
137
+
138
+ const countBeforeChange = evalCount;
139
+
140
+ act(() => {
141
+ rootMargin$.set("20px");
142
+ });
143
+
144
+ // rootMargin$ not tracked → evalCount unchanged
145
+ expect(evalCount).toBe(countBeforeChange);
146
+ });
147
+
148
+ it("inner Observable change IS reflected — Legend-State tracks inner fields directly (not via callback re-eval)", () => {
149
+ const rootMargin$ = observable("0px");
150
+ let evalCount = 0;
151
+
152
+ const { result } = renderHook(() => {
153
+ const computed$ = useObservable(() => {
154
+ evalCount++;
155
+ return get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ });
156
+ });
157
+ return { computed$ };
158
+ });
159
+
160
+ expect(result.current.computed$.rootMargin.get()).toBe("0px");
161
+ const countBeforeChange = evalCount;
162
+
163
+ act(() => {
164
+ rootMargin$.set("20px");
165
+ });
166
+
167
+ // Legend-State tracks inner Observable fields via field-level dep (NOT callback re-evaluation)
168
+ // → computed$.rootMargin updates to "20px" without re-running the outer callback
169
+ expect(result.current.computed$.rootMargin.get()).toBe("20px");
170
+ expect(evalCount).toBe(countBeforeChange); // outer callback NOT re-run
171
+ });
172
+ });
173
+
174
+ // =============================================================================
175
+ // useElementVisibility — per-field vs outer Observable reactivity comparison
176
+ // =============================================================================
177
+
178
+ describe("useElementVisibility — reactivity comparison", () => {
179
+ it("per-field rootMargin$ change → IntersectionObserver recreated ✓ (current implementation works)", () => {
180
+ const el = document.createElement("div");
181
+ const rootMargin$ = observable("0px");
182
+
183
+ renderHook(() =>
184
+ useElementVisibility(wrapEl(el), { rootMargin: rootMargin$ }),
185
+ );
186
+
187
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
188
+ expect(capturedInit?.rootMargin).toBe("0px");
189
+
190
+ MockIntersectionObserver.mockClear();
191
+ mockDisconnect.mockClear();
192
+
193
+ act(() => {
194
+ rootMargin$.set("20px");
195
+ });
196
+
197
+ // opts?.rootMargin = Observable<string> passed through to useIntersectionObserver
198
+ // useIntersectionObserver's useObserve calls get(rootMargin$) → dep registered
199
+ // → rootMargin$ change triggers setup() → IntersectionObserver recreated ✓
200
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
201
+ expect(capturedInit?.rootMargin).toBe("20px");
202
+ });
203
+
204
+ it("outer Observable options$.rootMargin child-field set → known Legend-State limitation (0×, not reactive)", () => {
205
+ const el = document.createElement("div");
206
+ const options$ = observable({ rootMargin: "0px" });
207
+
208
+ renderHook(() => useElementVisibility(wrapEl(el), options$));
209
+
210
+ expect(capturedInit?.rootMargin).toBe("0px");
211
+ MockIntersectionObserver.mockClear();
212
+ mockDisconnect.mockClear();
213
+
214
+ act(() => {
215
+ options$.rootMargin.set("20px");
216
+ });
217
+
218
+ // KNOWN LIMITATION: options$.rootMargin.set("20px") is a child-field mutation.
219
+ // options$.get() inside useObservable's compute fn does reference-equality tracking —
220
+ // the parent options$ object reference is unchanged, so the dep does NOT fire.
221
+ // opts_EV$ does NOT recompute → opts_EV$.rootMargin stays "0px" → 0× IO recreation.
222
+ //
223
+ // Workaround: use options$.set({ rootMargin: "20px" }) (whole-object replace)
224
+ // OR pass rootMargin as a per-field Observable: { rootMargin: observable("0px") }.
225
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(0);
226
+ });
227
+ });
228
+
229
+ // =============================================================================
230
+ // Case 3: useElementVisibility — scrollTarget (Element) options
231
+ // =============================================================================
232
+
233
+ describe("Case 3 — scrollTarget (Element) in useElementVisibility", () => {
234
+ it("plain HTMLElement as scrollTarget", () => {
235
+ const el = document.createElement("div");
236
+ const scrollContainer = document.createElement("div");
237
+
238
+ renderHook(() =>
239
+ useElementVisibility(wrapEl(el), { scrollTarget: wrapEl(scrollContainer) }),
240
+ );
241
+
242
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
243
+ expect.any(Function),
244
+ expect.objectContaining({ root: scrollContainer }),
245
+ );
246
+ });
247
+
248
+ it("outer Observable<Options> with scrollTarget — resolved at mount (snapshot)", () => {
249
+ const el = document.createElement("div");
250
+ const scrollContainer = document.createElement("div");
251
+ const options$ = observable({
252
+ scrollTarget: scrollContainer as any,
253
+ });
254
+
255
+ renderHook(() => useElementVisibility(wrapEl(el), options$));
256
+
257
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
258
+ expect.any(Function),
259
+ expect.objectContaining({ root: scrollContainer }),
260
+ );
261
+ });
262
+
263
+ it("Ref$ as scrollTarget — delays IntersectionObserver until Ref$ is mounted", () => {
264
+ const el = document.createElement("div");
265
+ const scrollContainer = document.createElement("div");
266
+
267
+ const { result } = renderHook(() => {
268
+ const scrollTarget$ = useRef$<HTMLElement>();
269
+ return {
270
+ scrollTarget$,
271
+ visibility: useElementVisibility(wrapEl(el), { scrollTarget: scrollTarget$ }),
272
+ };
273
+ });
274
+
275
+ // Ref$ is null → useIntersectionObserver's null guard skips setup
276
+ expect(MockIntersectionObserver).not.toHaveBeenCalled();
277
+
278
+ act(() => {
279
+ result.current.scrollTarget$(scrollContainer);
280
+ });
281
+
282
+ // Ref$ mounted → useIntersectionObserver's useObserve re-runs → setup() with root
283
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
284
+ expect.any(Function),
285
+ expect.objectContaining({ root: scrollContainer }),
286
+ );
287
+ });
288
+
289
+ it("Ref$ scrollTarget change → IntersectionObserver recreated with new root", () => {
290
+ const el = document.createElement("div");
291
+ const containerA = document.createElement("div");
292
+ const containerB = document.createElement("div");
293
+
294
+ const { result } = renderHook(() => {
295
+ const scrollTarget$ = useRef$<HTMLElement>();
296
+ return {
297
+ scrollTarget$,
298
+ visibility: useElementVisibility(wrapEl(el), { scrollTarget: scrollTarget$ }),
299
+ };
300
+ });
301
+
302
+ act(() => {
303
+ result.current.scrollTarget$(containerA);
304
+ });
305
+
306
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
307
+ expect.any(Function),
308
+ expect.objectContaining({ root: containerA }),
309
+ );
310
+
311
+ mockDisconnect.mockClear();
312
+ MockIntersectionObserver.mockClear();
313
+
314
+ act(() => {
315
+ result.current.scrollTarget$(containerB);
316
+ });
317
+
318
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
319
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
320
+ expect.any(Function),
321
+ expect.objectContaining({ root: containerB }),
322
+ );
323
+ });
324
+
325
+ it("per-field scrollTarget as Observable<HTMLElement | null> — null→element tracked (reliable pattern)", () => {
326
+ const el = document.createElement("div");
327
+ const container = document.createElement("div");
328
+ // Start with null — @legendapp/state reliably tracks null→element transitions.
329
+ // Use OpaqueObject to prevent Legend-State from deeply proxying the element.
330
+ const scrollTarget$ = observable<OpaqueObject<Element> | null>(null);
331
+
332
+ renderHook(() =>
333
+ useElementVisibility(wrapEl(el), { scrollTarget: scrollTarget$ }),
334
+ );
335
+
336
+ MockIntersectionObserver.mockClear();
337
+ mockDisconnect.mockClear();
338
+
339
+ act(() => {
340
+ scrollTarget$.set(ObservableHint.opaque(container));
341
+ });
342
+
343
+ // null→element: null root delays IO creation, no old IO to disconnect
344
+ // → new IO created with container as root
345
+ expect(mockDisconnect).not.toHaveBeenCalled();
346
+ expect(MockIntersectionObserver).toHaveBeenCalledWith(
347
+ expect.any(Function),
348
+ expect.objectContaining({ root: container }),
349
+ );
350
+ });
351
+ });
352
+
353
+ // =============================================================================
354
+ // Standard Pattern — useObservable(() => get(options), [options])
355
+ // Anti-pattern(snapshot)의 한계를 극복하는 올바른 패턴 검증
356
+ // =============================================================================
357
+
358
+ describe("Standard Pattern — useObservable(() => get(options), [options])", () => {
359
+ it("outer Observable options$ child-field change — opts$.rootMargin dep triggers IO recreated ✓", () => {
360
+ const el = document.createElement("div");
361
+ const options$ = observable({ rootMargin: "0px" });
362
+
363
+ renderHook(() => {
364
+ // Standard Pattern: outer Observable<Options>를 computed opts$로 정규화
365
+ // get(options$) → options$.get() in reactive context → dep registered
366
+ // opts$.rootMargin은 Observable<string> — useIntersectionObserver의 useObserve에서
367
+ // get(options.rootMargin) 호출 → dep registered → rootMargin 변경 시 setup() 재실행
368
+ const opts$ = useObservable(() => get(options$), [options$]);
369
+
370
+ useIntersectionObserver(wrapEl(el), () => {}, {
371
+ rootMargin: opts$.rootMargin,
372
+ });
373
+ });
374
+
375
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
376
+ expect(capturedInit?.rootMargin).toBe("0px");
377
+
378
+ MockIntersectionObserver.mockClear();
379
+ mockDisconnect.mockClear();
380
+
381
+ act(() => {
382
+ // 전체 객체 교체 — opts$ 재계산 → opts$.rootMargin 업데이트 → IO 재생성 ✓
383
+ // (child-field set은 useObservable 재평가를 트리거하지 않음 — Legend-State 동작)
384
+ options$.set({ rootMargin: "20px" });
385
+ });
386
+
387
+ // opts$.rootMargin이 Observable<string>으로 전달됨
388
+ // → options$ 교체 → opts$ 재계산 → opts$.rootMargin 업데이트
389
+ // → useObserve 재실행 → IntersectionObserver 재생성 ✓
390
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
391
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
392
+ expect(capturedInit?.rootMargin).toBe("20px");
393
+ });
394
+
395
+ it("outer Observable options$ 전체 교체 → IntersectionObserver recreated ✓", () => {
396
+ const el = document.createElement("div");
397
+ const options$ = observable<{ rootMargin: string; threshold?: number }>({
398
+ rootMargin: "0px",
399
+ });
400
+
401
+ renderHook(() => {
402
+ const opts$ = useObservable(() => get(options$), [options$]);
403
+ useIntersectionObserver(wrapEl(el), () => {}, {
404
+ rootMargin: opts$.rootMargin,
405
+ });
406
+ });
407
+
408
+ expect(capturedInit?.rootMargin).toBe("0px");
409
+ MockIntersectionObserver.mockClear();
410
+ mockDisconnect.mockClear();
411
+
412
+ act(() => {
413
+ // 전체 options 객체 교체
414
+ options$.set({ rootMargin: "50px", threshold: 0.5 });
415
+ });
416
+
417
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
418
+ expect(MockIntersectionObserver).toHaveBeenCalledTimes(1);
419
+ expect(capturedInit?.rootMargin).toBe("50px");
420
+ });
421
+
422
+ it("Standard Pattern with per-field Observable — 기존 per-field 동작도 유지됨 ✓", () => {
423
+ const el = document.createElement("div");
424
+ const rootMargin$ = observable("0px");
425
+
426
+ renderHook(() => {
427
+ // per-field Observable을 포함한 object — Standard Pattern 적용
428
+ // Legend-State auto-dereferences inner Observables → opts$.rootMargin은 Observable<string>
429
+ const opts$ = useObservable(
430
+ () => get<{ rootMargin: typeof rootMargin$ }>({ rootMargin: rootMargin$ }),
431
+ [rootMargin$],
432
+ );
433
+ useIntersectionObserver(wrapEl(el), () => {}, {
434
+ rootMargin: opts$.rootMargin,
435
+ });
436
+ });
437
+
438
+ expect(capturedInit?.rootMargin).toBe("0px");
439
+ MockIntersectionObserver.mockClear();
440
+ mockDisconnect.mockClear();
441
+
442
+ act(() => {
443
+ rootMargin$.set("30px");
444
+ });
445
+
446
+ // rootMargin$ 변경 시 두 dep 경로가 모두 fire될 수 있음:
447
+ // 1) opts$.rootMargin (auto-dereferenced inner dep) → useObserve 재실행
448
+ // 2) useObservable deps array [rootMargin$] → 재계산
449
+ // 결과적으로 IO가 재생성되고 최종 rootMargin은 "30px" ✓
450
+ expect(MockIntersectionObserver).toHaveBeenCalled();
451
+ expect(capturedInit?.rootMargin).toBe("30px");
452
+ });
453
+ });
@@ -0,0 +1,97 @@
1
+ import { Computed } from "@legendapp/state/react";
2
+ import { useRef$ } from "../useRef$";
3
+ import { useElementVisibility } from ".";
4
+
5
+ export default function UseElementVisibilityDemo() {
6
+ const el$ = useRef$<HTMLDivElement>();
7
+ const isVisible$ = useElementVisibility(el$, { threshold: 0.5 });
8
+
9
+ return (
10
+ <div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
11
+ {/* Status bar */}
12
+ <div
13
+ style={{
14
+ display: "flex",
15
+ alignItems: "center",
16
+ gap: "16px",
17
+ fontFamily: "monospace",
18
+ fontSize: "14px",
19
+ padding: "8px 12px",
20
+ background: "var(--sl-color-gray-6, #f1f5f9)",
21
+ borderRadius: "6px",
22
+ }}
23
+ >
24
+ <Computed>
25
+ {() => (
26
+ <span>
27
+ isVisible:{" "}
28
+ <strong
29
+ style={{
30
+ color: isVisible$.get()
31
+ ? "var(--sl-color-green, #22c55e)"
32
+ : "inherit",
33
+ }}
34
+ >
35
+ {String(isVisible$.get())}
36
+ </strong>
37
+ </span>
38
+ )}
39
+ </Computed>
40
+ </div>
41
+
42
+ {/* Scrollable container */}
43
+ <div
44
+ style={{
45
+ height: "200px",
46
+ overflowY: "auto",
47
+ border: "1px solid var(--sl-color-gray-5, #cbd5e1)",
48
+ borderRadius: "6px",
49
+ position: "relative",
50
+ }}
51
+ >
52
+ <div
53
+ style={{
54
+ display: "flex",
55
+ alignItems: "center",
56
+ justifyContent: "center",
57
+ height: "200px",
58
+ color: "var(--sl-color-gray-3, #94a3b8)",
59
+ fontSize: "13px",
60
+ fontFamily: "monospace",
61
+ }}
62
+ >
63
+ ↓ scroll down
64
+ </div>
65
+
66
+ <Computed>
67
+ {() => (
68
+ <div
69
+ ref={el$}
70
+ style={{
71
+ margin: "0 16px",
72
+ padding: "20px",
73
+ borderRadius: "6px",
74
+ textAlign: "center",
75
+ fontFamily: "monospace",
76
+ fontSize: "13px",
77
+ transition: "background 0.2s, border-color 0.2s",
78
+ border: `2px solid ${
79
+ isVisible$.get()
80
+ ? "var(--sl-color-green, #22c55e)"
81
+ : "var(--sl-color-gray-4, #94a3b8)"
82
+ }`,
83
+ background: isVisible$.get()
84
+ ? "var(--sl-color-green-low, #dcfce7)"
85
+ : "transparent",
86
+ }}
87
+ >
88
+ target element
89
+ </div>
90
+ )}
91
+ </Computed>
92
+
93
+ <div style={{ height: "140px" }} />
94
+ </div>
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: useElementVisibility
3
+ category: elements
4
+ ---
5
+
6
+ Tracks whether a DOM element is visible within the viewport (or a specified scroll container).
7
+ Returns a reactive `Observable<boolean>` that updates automatically via the [IntersectionObserver API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
8
+
9
+ All option values accept either a plain value or an `Observable<T>`.
10
+
11
+ ## Demo
12
+
13
+ ## Usage
14
+
15
+ ```tsx twoslash
16
+ // @noErrors
17
+ import { useRef$, Ref$, useElementVisibility } from '@usels/core'
18
+
19
+ function Component() {
20
+ const el$ = useRef$<HTMLDivElement>()
21
+ const isVisible$ = useElementVisibility(el$)
22
+
23
+ return <div ref={el$} />
24
+ }
25
+ ```
26
+
27
+ ### With initial value
28
+
29
+ ```tsx twoslash
30
+ // @noErrors
31
+ import { useRef$, Ref$, useElementVisibility } from '@usels/core'
32
+ declare const el$: Ref$<HTMLDivElement>
33
+ // ---cut---
34
+ const isVisible$ = useElementVisibility(el$, { initialValue: true })
35
+ ```
36
+
37
+ ### Stop after first visible
38
+
39
+ Use `once: true` to automatically stop observing after the element becomes visible for the first time:
40
+
41
+ ```tsx twoslash
42
+ // @noErrors
43
+ import { useRef$, Ref$, useElementVisibility } from '@usels/core'
44
+ declare const el$: Ref$<HTMLDivElement>
45
+ // ---cut---
46
+ const isVisible$ = useElementVisibility(el$, { once: true })
47
+ ```
48
+
49
+ ### Custom scroll container
50
+
51
+ Pass a `scrollTarget` to observe intersection within a scrollable container instead of the viewport:
52
+
53
+ ```tsx twoslash
54
+ // @noErrors
55
+ import { useRef$, Ref$, useElementVisibility } from '@usels/core'
56
+ declare const el$: Ref$<HTMLDivElement>
57
+ // ---cut---
58
+ const container$ = useRef$<HTMLDivElement>()
59
+ const isVisible$ = useElementVisibility(el$, { scrollTarget: container$ })
60
+ ```
61
+
62
+ ### Threshold and rootMargin
63
+
64
+ ```tsx twoslash
65
+ // @noErrors
66
+ import { useRef$, Ref$, useElementVisibility } from '@usels/core'
67
+ declare const el$: Ref$<HTMLDivElement>
68
+ // ---cut---
69
+ const isVisible$ = useElementVisibility(el$, {
70
+ threshold: 0.5,
71
+ rootMargin: '0px 0px -100px 0px',
72
+ })
73
+ ```
74
+
75
+ ### Reactive options
76
+
77
+ All options accept `Observable<T>` for reactive control:
78
+
79
+ ```tsx twoslash
80
+ // @noErrors
81
+ import { observable } from '@legendapp/state'
82
+ import { useRef$, Ref$, useElementVisibility } from '@usels/core'
83
+ declare const el$: Ref$<HTMLDivElement>
84
+ // ---cut---
85
+ const threshold$ = observable<number | number[]>(0.5)
86
+ const rootMargin$ = observable('0px')
87
+ const once$ = observable(false)
88
+
89
+ const isVisible$ = useElementVisibility(el$, {
90
+ threshold: threshold$,
91
+ rootMargin: rootMargin$,
92
+ once: once$,
93
+ })
94
+
95
+ // later — update reactively
96
+ threshold$.set(0.75)
97
+ rootMargin$.set('-50px 0px')
98
+ ```