foldkit-primitives 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (422) hide show
  1. package/dist/device/fullscreen.d.ts +28 -0
  2. package/dist/device/fullscreen.d.ts.map +1 -0
  3. package/dist/device/fullscreen.js +69 -0
  4. package/dist/device/fullscreen.js.map +1 -0
  5. package/dist/device/geolocation.d.ts +56 -0
  6. package/dist/device/geolocation.d.ts.map +1 -0
  7. package/dist/device/geolocation.js +87 -0
  8. package/dist/device/geolocation.js.map +1 -0
  9. package/dist/device/index.d.ts +5 -0
  10. package/dist/device/index.js +6 -0
  11. package/dist/device/media-devices.d.ts +88 -0
  12. package/dist/device/media-devices.d.ts.map +1 -0
  13. package/dist/device/media-devices.js +117 -0
  14. package/dist/device/media-devices.js.map +1 -0
  15. package/dist/device/media-stream.d.ts +93 -0
  16. package/dist/device/media-stream.d.ts.map +1 -0
  17. package/dist/device/media-stream.js +106 -0
  18. package/dist/device/media-stream.js.map +1 -0
  19. package/dist/device/permissions.d.ts +106 -0
  20. package/dist/device/permissions.d.ts.map +1 -0
  21. package/dist/device/permissions.js +143 -0
  22. package/dist/device/permissions.js.map +1 -0
  23. package/dist/dom/autofocus.d.ts +15 -0
  24. package/dist/dom/autofocus.d.ts.map +1 -0
  25. package/dist/dom/autofocus.js +22 -0
  26. package/dist/dom/autofocus.js.map +1 -0
  27. package/dist/dom/clipboard.d.ts +30 -0
  28. package/dist/dom/clipboard.d.ts.map +1 -0
  29. package/dist/dom/clipboard.js +50 -0
  30. package/dist/dom/clipboard.js.map +1 -0
  31. package/dist/dom/editable-text.d.ts +72 -0
  32. package/dist/dom/editable-text.d.ts.map +1 -0
  33. package/dist/dom/editable-text.js +232 -0
  34. package/dist/dom/editable-text.js.map +1 -0
  35. package/dist/dom/fluid-hover.d.ts +93 -0
  36. package/dist/dom/fluid-hover.d.ts.map +1 -0
  37. package/dist/dom/fluid-hover.js +294 -0
  38. package/dist/dom/fluid-hover.js.map +1 -0
  39. package/dist/dom/focus-scope.d.ts +21 -0
  40. package/dist/dom/focus-scope.d.ts.map +1 -0
  41. package/dist/dom/focus-scope.js +93 -0
  42. package/dist/dom/focus-scope.js.map +1 -0
  43. package/dist/dom/follow-tab-stop.d.ts +11 -0
  44. package/dist/dom/follow-tab-stop.d.ts.map +1 -0
  45. package/dist/dom/follow-tab-stop.js +54 -0
  46. package/dist/dom/follow-tab-stop.js.map +1 -0
  47. package/dist/dom/image-decode.d.ts +27 -0
  48. package/dist/dom/image-decode.d.ts.map +1 -0
  49. package/dist/dom/image-decode.js +50 -0
  50. package/dist/dom/image-decode.js.map +1 -0
  51. package/dist/dom/index.d.ts +24 -0
  52. package/dist/dom/index.js +25 -0
  53. package/dist/dom/input-mask.d.ts +35 -0
  54. package/dist/dom/input-mask.d.ts.map +1 -0
  55. package/dist/dom/input-mask.js +90 -0
  56. package/dist/dom/input-mask.js.map +1 -0
  57. package/dist/dom/keep-in-view.d.ts +16 -0
  58. package/dist/dom/keep-in-view.d.ts.map +1 -0
  59. package/dist/dom/keep-in-view.js +38 -0
  60. package/dist/dom/keep-in-view.js.map +1 -0
  61. package/dist/dom/keyboard-inset.d.ts +29 -0
  62. package/dist/dom/keyboard-inset.d.ts.map +1 -0
  63. package/dist/dom/keyboard-inset.js +45 -0
  64. package/dist/dom/keyboard-inset.js.map +1 -0
  65. package/dist/dom/layers.d.ts +6 -0
  66. package/dist/dom/layers.d.ts.map +1 -0
  67. package/dist/dom/layers.js +35 -0
  68. package/dist/dom/layers.js.map +1 -0
  69. package/dist/dom/measure.d.ts +42 -0
  70. package/dist/dom/measure.d.ts.map +1 -0
  71. package/dist/dom/measure.js +123 -0
  72. package/dist/dom/measure.js.map +1 -0
  73. package/dist/dom/move.d.ts +45 -0
  74. package/dist/dom/move.d.ts.map +1 -0
  75. package/dist/dom/move.js +96 -0
  76. package/dist/dom/move.js.map +1 -0
  77. package/dist/dom/pointed-links.d.ts +13 -0
  78. package/dist/dom/pointed-links.d.ts.map +1 -0
  79. package/dist/dom/pointed-links.js +59 -0
  80. package/dist/dom/pointed-links.js.map +1 -0
  81. package/dist/dom/pointer-drag.d.ts +132 -0
  82. package/dist/dom/pointer-drag.d.ts.map +1 -0
  83. package/dist/dom/pointer-drag.js +307 -0
  84. package/dist/dom/pointer-drag.js.map +1 -0
  85. package/dist/dom/pointer-position.d.ts +133 -0
  86. package/dist/dom/pointer-position.d.ts.map +1 -0
  87. package/dist/dom/pointer-position.js +173 -0
  88. package/dist/dom/pointer-position.js.map +1 -0
  89. package/dist/dom/script-loader.d.ts +12 -0
  90. package/dist/dom/script-loader.d.ts.map +1 -0
  91. package/dist/dom/script-loader.js +54 -0
  92. package/dist/dom/script-loader.js.map +1 -0
  93. package/dist/dom/scroll-bars.d.ts +52 -0
  94. package/dist/dom/scroll-bars.d.ts.map +1 -0
  95. package/dist/dom/scroll-bars.js +259 -0
  96. package/dist/dom/scroll-bars.js.map +1 -0
  97. package/dist/dom/scroll-settle.d.ts +33 -0
  98. package/dist/dom/scroll-settle.d.ts.map +1 -0
  99. package/dist/dom/scroll-settle.js +84 -0
  100. package/dist/dom/scroll-settle.js.map +1 -0
  101. package/dist/dom/scroll.d.ts +21 -0
  102. package/dist/dom/scroll.d.ts.map +1 -0
  103. package/dist/dom/scroll.js +141 -0
  104. package/dist/dom/scroll.js.map +1 -0
  105. package/dist/dom/share.d.ts +19 -0
  106. package/dist/dom/share.d.ts.map +1 -0
  107. package/dist/dom/share.js +41 -0
  108. package/dist/dom/share.js.map +1 -0
  109. package/dist/dom/swipe.d.ts +121 -0
  110. package/dist/dom/swipe.d.ts.map +1 -0
  111. package/dist/dom/swipe.js +291 -0
  112. package/dist/dom/swipe.js.map +1 -0
  113. package/dist/dom/targets.d.ts +45 -0
  114. package/dist/dom/targets.d.ts.map +1 -0
  115. package/dist/dom/targets.js +70 -0
  116. package/dist/dom/targets.js.map +1 -0
  117. package/dist/dom/track-box.d.ts +61 -0
  118. package/dist/dom/track-box.d.ts.map +1 -0
  119. package/dist/dom/track-box.js +131 -0
  120. package/dist/dom/track-box.js.map +1 -0
  121. package/dist/events/active-element.d.ts +15 -0
  122. package/dist/events/active-element.d.ts.map +1 -0
  123. package/dist/events/active-element.js +33 -0
  124. package/dist/events/active-element.js.map +1 -0
  125. package/dist/events/hotkeys.d.ts +22 -0
  126. package/dist/events/hotkeys.d.ts.map +1 -0
  127. package/dist/events/hotkeys.js +56 -0
  128. package/dist/events/hotkeys.js.map +1 -0
  129. package/dist/events/idle.d.ts +29 -0
  130. package/dist/events/idle.d.ts.map +1 -0
  131. package/dist/events/idle.js +51 -0
  132. package/dist/events/idle.js.map +1 -0
  133. package/dist/events/index.d.ts +9 -0
  134. package/dist/events/index.js +10 -0
  135. package/dist/{input-modality-BY-bSrEc.d.mts → events/input-modality.d.ts} +9 -12
  136. package/dist/events/input-modality.d.ts.map +1 -0
  137. package/dist/{input-modality-BXD383jg.mjs → events/input-modality.js} +3 -4
  138. package/dist/events/input-modality.js.map +1 -0
  139. package/dist/events/keyboard.d.ts +30 -0
  140. package/dist/events/keyboard.d.ts.map +1 -0
  141. package/dist/events/keyboard.js +55 -0
  142. package/dist/events/keyboard.js.map +1 -0
  143. package/dist/events/pointer.d.ts +15 -0
  144. package/dist/events/pointer.d.ts.map +1 -0
  145. package/dist/events/pointer.js +28 -0
  146. package/dist/events/pointer.js.map +1 -0
  147. package/dist/events/scroll.d.ts +16 -0
  148. package/dist/events/scroll.d.ts.map +1 -0
  149. package/dist/events/scroll.js +29 -0
  150. package/dist/events/scroll.js.map +1 -0
  151. package/dist/events/visibility.d.ts +25 -0
  152. package/dist/events/visibility.d.ts.map +1 -0
  153. package/dist/events/visibility.js +28 -0
  154. package/dist/events/visibility.js.map +1 -0
  155. package/dist/events/window-size.d.ts +30 -0
  156. package/dist/events/window-size.d.ts.map +1 -0
  157. package/dist/events/window-size.js +63 -0
  158. package/dist/events/window-size.js.map +1 -0
  159. package/dist/index.d.ts +9 -0
  160. package/dist/index.js +10 -0
  161. package/dist/interaction/bundle-behavior.d.ts +37 -0
  162. package/dist/interaction/bundle-behavior.d.ts.map +1 -0
  163. package/dist/interaction/bundle-behavior.js +34 -0
  164. package/dist/interaction/bundle-behavior.js.map +1 -0
  165. package/dist/interaction/dismiss-layer.d.ts +100 -0
  166. package/dist/interaction/dismiss-layer.d.ts.map +1 -0
  167. package/dist/interaction/dismiss-layer.js +140 -0
  168. package/dist/interaction/dismiss-layer.js.map +1 -0
  169. package/dist/interaction/editable-text.d.ts +13 -0
  170. package/dist/interaction/editable-text.d.ts.map +1 -0
  171. package/dist/interaction/editable-text.js +17 -0
  172. package/dist/interaction/editable-text.js.map +1 -0
  173. package/dist/interaction/focus-scope.d.ts +21 -0
  174. package/dist/interaction/focus-scope.d.ts.map +1 -0
  175. package/dist/interaction/focus-scope.js +25 -0
  176. package/dist/interaction/focus-scope.js.map +1 -0
  177. package/dist/interaction/focus-visible.d.ts +9 -0
  178. package/dist/interaction/focus-visible.d.ts.map +1 -0
  179. package/dist/interaction/focus-visible.js +10 -0
  180. package/dist/interaction/focus-visible.js.map +1 -0
  181. package/dist/interaction/grid-navigation.d.ts +73 -0
  182. package/dist/interaction/grid-navigation.d.ts.map +1 -0
  183. package/dist/interaction/grid-navigation.js +137 -0
  184. package/dist/interaction/grid-navigation.js.map +1 -0
  185. package/dist/interaction/index.d.ts +21 -0
  186. package/dist/interaction/index.js +21 -0
  187. package/dist/interaction/layers.d.ts +12 -0
  188. package/dist/interaction/layers.d.ts.map +1 -0
  189. package/dist/interaction/layers.js +14 -0
  190. package/dist/interaction/layers.js.map +1 -0
  191. package/dist/interaction/list-navigation.d.ts +105 -0
  192. package/dist/interaction/list-navigation.d.ts.map +1 -0
  193. package/dist/interaction/list-navigation.js +168 -0
  194. package/dist/interaction/list-navigation.js.map +1 -0
  195. package/dist/interaction/live-announce.d.ts +70 -0
  196. package/dist/interaction/live-announce.d.ts.map +1 -0
  197. package/dist/interaction/live-announce.js +115 -0
  198. package/dist/interaction/live-announce.js.map +1 -0
  199. package/dist/interaction/long-press.d.ts +102 -0
  200. package/dist/interaction/long-press.d.ts.map +1 -0
  201. package/dist/interaction/long-press.js +115 -0
  202. package/dist/interaction/long-press.js.map +1 -0
  203. package/dist/interaction/move.d.ts +10 -0
  204. package/dist/interaction/move.d.ts.map +1 -0
  205. package/dist/interaction/move.js +17 -0
  206. package/dist/interaction/move.js.map +1 -0
  207. package/dist/interaction/overlay.d.ts +45 -0
  208. package/dist/interaction/overlay.d.ts.map +1 -0
  209. package/dist/interaction/overlay.js +57 -0
  210. package/dist/interaction/overlay.js.map +1 -0
  211. package/dist/interaction/placing.d.ts +86 -0
  212. package/dist/interaction/placing.d.ts.map +1 -0
  213. package/dist/interaction/placing.js +239 -0
  214. package/dist/interaction/placing.js.map +1 -0
  215. package/dist/interaction/pointer-drag.d.ts +23 -0
  216. package/dist/interaction/pointer-drag.d.ts.map +1 -0
  217. package/dist/interaction/pointer-drag.js +21 -0
  218. package/dist/interaction/pointer-drag.js.map +1 -0
  219. package/dist/interaction/press.d.ts +152 -0
  220. package/dist/interaction/press.d.ts.map +1 -0
  221. package/dist/interaction/press.js +229 -0
  222. package/dist/interaction/press.js.map +1 -0
  223. package/dist/interaction/roving-tabindex.d.ts +85 -0
  224. package/dist/interaction/roving-tabindex.d.ts.map +1 -0
  225. package/dist/interaction/roving-tabindex.js +142 -0
  226. package/dist/interaction/roving-tabindex.js.map +1 -0
  227. package/dist/interaction/selection.d.ts +72 -0
  228. package/dist/interaction/selection.d.ts.map +1 -0
  229. package/dist/interaction/selection.js +129 -0
  230. package/dist/interaction/selection.js.map +1 -0
  231. package/dist/interaction/targets.d.ts +15 -0
  232. package/dist/interaction/targets.d.ts.map +1 -0
  233. package/dist/interaction/targets.js +20 -0
  234. package/dist/interaction/targets.js.map +1 -0
  235. package/dist/interaction/tree-navigation.d.ts +111 -0
  236. package/dist/interaction/tree-navigation.d.ts.map +1 -0
  237. package/dist/interaction/tree-navigation.js +207 -0
  238. package/dist/interaction/tree-navigation.js.map +1 -0
  239. package/dist/interaction/typeahead.d.ts +73 -0
  240. package/dist/interaction/typeahead.d.ts.map +1 -0
  241. package/dist/interaction/typeahead.js +113 -0
  242. package/dist/interaction/typeahead.js.map +1 -0
  243. package/dist/internal.d.ts +16 -0
  244. package/dist/internal.d.ts.map +1 -0
  245. package/dist/{internal-DP-xLKbT.mjs → internal.js} +8 -7
  246. package/dist/internal.js.map +1 -0
  247. package/dist/media/breakpoints.d.ts +37 -0
  248. package/dist/media/breakpoints.d.ts.map +1 -0
  249. package/dist/media/breakpoints.js +64 -0
  250. package/dist/media/breakpoints.js.map +1 -0
  251. package/dist/media/index.d.ts +3 -0
  252. package/dist/media/index.js +4 -0
  253. package/dist/media/mediaquery.d.ts +40 -0
  254. package/dist/media/mediaquery.d.ts.map +1 -0
  255. package/dist/media/mediaquery.js +42 -0
  256. package/dist/media/mediaquery.js.map +1 -0
  257. package/dist/media/platform.d.ts +30 -0
  258. package/dist/media/platform.d.ts.map +1 -0
  259. package/dist/media/platform.js +50 -0
  260. package/dist/media/platform.js.map +1 -0
  261. package/dist/motion/index.d.ts +5 -0
  262. package/dist/motion/index.js +5 -0
  263. package/dist/motion/motion.d.ts +16 -0
  264. package/dist/motion/motion.d.ts.map +1 -0
  265. package/dist/motion/motion.js +25 -0
  266. package/dist/motion/motion.js.map +1 -0
  267. package/dist/motion/presence.d.ts +37 -0
  268. package/dist/motion/presence.d.ts.map +1 -0
  269. package/dist/motion/presence.js +65 -0
  270. package/dist/motion/presence.js.map +1 -0
  271. package/dist/motion/spring.d.ts +50 -0
  272. package/dist/motion/spring.d.ts.map +1 -0
  273. package/dist/motion/spring.js +92 -0
  274. package/dist/motion/spring.js.map +1 -0
  275. package/dist/motion/tween.d.ts +44 -0
  276. package/dist/motion/tween.d.ts.map +1 -0
  277. package/dist/motion/tween.js +65 -0
  278. package/dist/motion/tween.js.map +1 -0
  279. package/dist/net/broadcast.d.ts +39 -0
  280. package/dist/net/broadcast.d.ts.map +1 -0
  281. package/dist/net/broadcast.js +73 -0
  282. package/dist/net/broadcast.js.map +1 -0
  283. package/dist/net/index.d.ts +5 -0
  284. package/dist/net/index.js +6 -0
  285. package/dist/net/online.d.ts +24 -0
  286. package/dist/net/online.d.ts.map +1 -0
  287. package/dist/net/online.js +26 -0
  288. package/dist/net/online.js.map +1 -0
  289. package/dist/net/sharedHost.d.ts +64 -0
  290. package/dist/net/sharedHost.d.ts.map +1 -0
  291. package/dist/net/sharedHost.js +124 -0
  292. package/dist/net/sharedHost.js.map +1 -0
  293. package/dist/net/sse.d.ts +115 -0
  294. package/dist/net/sse.d.ts.map +1 -0
  295. package/dist/net/sse.js +138 -0
  296. package/dist/net/sse.js.map +1 -0
  297. package/dist/net/websocket.d.ts +172 -0
  298. package/dist/net/websocket.d.ts.map +1 -0
  299. package/dist/net/websocket.js +199 -0
  300. package/dist/net/websocket.js.map +1 -0
  301. package/dist/observers/bounds.d.ts +24 -0
  302. package/dist/observers/bounds.d.ts.map +1 -0
  303. package/dist/observers/bounds.js +51 -0
  304. package/dist/observers/bounds.js.map +1 -0
  305. package/dist/observers/index.d.ts +4 -0
  306. package/dist/observers/index.js +5 -0
  307. package/dist/observers/intersection.d.ts +18 -0
  308. package/dist/observers/intersection.d.ts.map +1 -0
  309. package/dist/observers/intersection.js +33 -0
  310. package/dist/observers/intersection.js.map +1 -0
  311. package/dist/observers/mutation.d.ts +25 -0
  312. package/dist/observers/mutation.d.ts.map +1 -0
  313. package/dist/observers/mutation.js +53 -0
  314. package/dist/observers/mutation.js.map +1 -0
  315. package/dist/observers/resize.d.ts +18 -0
  316. package/dist/observers/resize.d.ts.map +1 -0
  317. package/dist/observers/resize.js +33 -0
  318. package/dist/observers/resize.js.map +1 -0
  319. package/dist/state/history.d.ts +115 -0
  320. package/dist/state/history.d.ts.map +1 -0
  321. package/dist/state/history.js +167 -0
  322. package/dist/state/history.js.map +1 -0
  323. package/dist/state/index.d.ts +6 -0
  324. package/dist/state/index.js +7 -0
  325. package/dist/state/locale.d.ts +27 -0
  326. package/dist/state/locale.d.ts.map +1 -0
  327. package/dist/state/locale.js +25 -0
  328. package/dist/state/locale.js.map +1 -0
  329. package/dist/state/pagination.d.ts +53 -0
  330. package/dist/state/pagination.d.ts.map +1 -0
  331. package/dist/state/pagination.js +72 -0
  332. package/dist/state/pagination.js.map +1 -0
  333. package/dist/state/range.d.ts +9 -0
  334. package/dist/state/range.d.ts.map +1 -0
  335. package/dist/state/range.js +18 -0
  336. package/dist/state/range.js.map +1 -0
  337. package/dist/state/selection.d.ts +47 -0
  338. package/dist/state/selection.d.ts.map +1 -0
  339. package/dist/state/selection.js +39 -0
  340. package/dist/state/selection.js.map +1 -0
  341. package/dist/state/virtual.d.ts +201 -0
  342. package/dist/state/virtual.d.ts.map +1 -0
  343. package/dist/state/{index.mjs → virtual.js} +11 -313
  344. package/dist/state/virtual.js.map +1 -0
  345. package/dist/time/debounce.d.ts +43 -0
  346. package/dist/time/debounce.d.ts.map +1 -0
  347. package/dist/time/debounce.js +65 -0
  348. package/dist/time/debounce.js.map +1 -0
  349. package/dist/time/index.d.ts +6 -0
  350. package/dist/time/index.js +7 -0
  351. package/dist/time/interval.d.ts +37 -0
  352. package/dist/time/interval.d.ts.map +1 -0
  353. package/dist/time/interval.js +52 -0
  354. package/dist/time/interval.js.map +1 -0
  355. package/dist/time/relative.d.ts +8 -0
  356. package/dist/time/relative.d.ts.map +1 -0
  357. package/dist/time/relative.js +27 -0
  358. package/dist/time/relative.js.map +1 -0
  359. package/dist/time/throttle.d.ts +38 -0
  360. package/dist/time/throttle.d.ts.map +1 -0
  361. package/dist/time/throttle.js +39 -0
  362. package/dist/time/throttle.js.map +1 -0
  363. package/dist/time/ticks.d.ts +21 -0
  364. package/dist/time/ticks.d.ts.map +1 -0
  365. package/dist/time/ticks.js +30 -0
  366. package/dist/time/ticks.js.map +1 -0
  367. package/dist/time/timer.d.ts +32 -0
  368. package/dist/time/timer.d.ts.map +1 -0
  369. package/dist/time/timer.js +50 -0
  370. package/dist/time/timer.js.map +1 -0
  371. package/package.json +31 -31
  372. package/dist/device/index.d.mts +0 -345
  373. package/dist/device/index.d.mts.map +0 -1
  374. package/dist/device/index.mjs +0 -498
  375. package/dist/device/index.mjs.map +0 -1
  376. package/dist/dom/index.d.mts +0 -750
  377. package/dist/dom/index.d.mts.map +0 -1
  378. package/dist/dom/index.mjs +0 -1857
  379. package/dist/dom/index.mjs.map +0 -1
  380. package/dist/editable-text-1D31qthY.d.mts +0 -319
  381. package/dist/editable-text-1D31qthY.d.mts.map +0 -1
  382. package/dist/events/index.d.mts +0 -184
  383. package/dist/events/index.d.mts.map +0 -1
  384. package/dist/events/index.mjs +0 -315
  385. package/dist/events/index.mjs.map +0 -1
  386. package/dist/follow-tab-stop-Caog2qg1.mjs +0 -863
  387. package/dist/follow-tab-stop-Caog2qg1.mjs.map +0 -1
  388. package/dist/index-CmmxqSEi.d.mts +0 -149
  389. package/dist/index-CmmxqSEi.d.mts.map +0 -1
  390. package/dist/index.d.mts +0 -12
  391. package/dist/index.mjs +0 -12
  392. package/dist/input-modality-BXD383jg.mjs.map +0 -1
  393. package/dist/input-modality-BY-bSrEc.d.mts.map +0 -1
  394. package/dist/interaction/index.d.mts +0 -1194
  395. package/dist/interaction/index.d.mts.map +0 -1
  396. package/dist/interaction/index.mjs +0 -2012
  397. package/dist/interaction/index.mjs.map +0 -1
  398. package/dist/internal-DP-xLKbT.mjs.map +0 -1
  399. package/dist/media/index.d.mts +0 -109
  400. package/dist/media/index.d.mts.map +0 -1
  401. package/dist/media/index.mjs +0 -149
  402. package/dist/media/index.mjs.map +0 -1
  403. package/dist/motion/index.d.mts +0 -2
  404. package/dist/motion/index.mjs +0 -1
  405. package/dist/motion-DmFDppRn.mjs +0 -240
  406. package/dist/motion-DmFDppRn.mjs.map +0 -1
  407. package/dist/net/index.d.mts +0 -348
  408. package/dist/net/index.d.mts.map +0 -1
  409. package/dist/net/index.mjs +0 -535
  410. package/dist/net/index.mjs.map +0 -1
  411. package/dist/observers/index.d.mts +0 -84
  412. package/dist/observers/index.d.mts.map +0 -1
  413. package/dist/observers/index.mjs +0 -159
  414. package/dist/observers/index.mjs.map +0 -1
  415. package/dist/rolldown-runtime-D7D4PA-g.mjs +0 -13
  416. package/dist/state/index.d.mts +0 -444
  417. package/dist/state/index.d.mts.map +0 -1
  418. package/dist/state/index.mjs.map +0 -1
  419. package/dist/time/index.d.mts +0 -179
  420. package/dist/time/index.d.mts.map +0 -1
  421. package/dist/time/index.mjs +0 -242
  422. package/dist/time/index.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-navigation.js","names":[],"sources":["../../src/interaction/tree-navigation.ts"],"sourcesContent":["/**\n * Keyboard navigation of a tree (a file explorer, a page's layers, a nested\n * menu), after the WAI-ARIA tree pattern: one tab stop, Up and Down through the\n * rows that are showing, Right to open a row or step into it, Left to close it\n * or step out to its parent, Home and End to the first and last row.\n *\n * The Model slice is the current row's id and which rows are open. Openness is\n * stored as the rows `toggled` away from a default, so a layers panel that\n * starts with everything open and a file tree that starts closed are the same\n * bundle with a different `openByDefault`, and neither stores every row.\n *\n * Which rows exist is the view's to say, each with its parent, in tree order.\n * What a key does is a pure function of those rows and the Model; the Behavior\n * wires it to the slots and writes the tree's ARIA attributes.\n */\nimport { Option, Schema } from 'effect'\nimport type { HtmlBuilder, KeyboardModifiers } from 'foldkit/html'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\nimport type { Declared } from 'foldkit-bundle'\nimport { Behavior, Capability, type SlotItem } from 'foldkit-mixins'\nimport { FollowTabStop } from '../dom/follow-tab-stop.js'\nimport { idSelector } from './roving-tabindex.js'\nimport { perInput } from '../internal.js'\n\nexport const Model = Schema.Struct({\n /** The current row's id, or `null` before any row has been focused. */\n current: Schema.NullOr(Schema.String),\n /** The rows whose open state differs from `openByDefault`. */\n toggled: Schema.Array(Schema.String),\n})\nexport type Model = typeof Model.Type\n\nexport const Message = defineMessageUnion({\n /** A row became current: the user focused it, or a key moved there. */\n Focused: { id: Schema.String },\n Opened: { id: Schema.String },\n Closed: { id: Schema.String },\n})\nexport type Message = typeof Message.Type\n\nexport const Args = Schema.Struct({\n /** Whether a row with children starts open. */\n openByDefault: Schema.Boolean,\n})\nexport type Args = typeof Args.Type\n\n/** Whether a row is open, given the default and what was toggled. */\nexport const isOpen = (model: Model, args: Args, id: string): boolean =>\n args.openByDefault !== model.toggled.includes(id)\n\nconst setOpen = (model: Model, args: Args, id: string, open: boolean): Model =>\n isOpen(model, args, id) === open\n ? model\n : {\n ...model,\n toggled: model.toggled.includes(id)\n ? model.toggled.filter(toggled => toggled !== id)\n : [...model.toggled, id],\n }\n\nexport const bundle = Bundle.make('TreeNavigation', {\n Model,\n Message,\n args: Args,\n init: () => ({ model: { current: null, toggled: [] } }),\n update: (model, message, args) => {\n switch (message._tag) {\n // Moving focus by key dispatches `Focused`, and the row's `OnFocus` reports it again.\n case 'Focused':\n return model.current === message.id\n ? { model }\n : { model: { ...model, current: message.id } }\n case 'Opened':\n return { model: setOpen(model, args, message.id, true) }\n case 'Closed':\n return { model: setOpen(model, args, message.id, false) }\n }\n },\n})\n\n/** One row of the tree, in tree order: a parent comes before its children. */\nexport interface Row {\n readonly id: string\n /** The parent's id, or `null` for a top-level row. */\n readonly parent: string | null\n /** Whether it has children, so it can be opened and closed. */\n readonly branch: boolean\n readonly disabled?: boolean\n}\n\n/** A row as it is showing: where it sits, for its ARIA attributes. */\nexport interface Shown extends Row {\n /** 1 for a top-level row. */\n readonly level: number\n /** Its place among its siblings, from 1, and how many there are. */\n readonly position: number\n readonly siblings: number\n}\n\n/**\n * The rows that are showing: every row whose ancestors are all open, with its\n * level and its place among its siblings. A row whose parent is not a row is\n * treated as top-level.\n */\nexport const shown = (rows: ReadonlyArray<Row>, model: Model, args: Args): ReadonlyArray<Shown> => {\n const byId = new Map(rows.map(row => [row.id, row]))\n // Each row's place among its siblings, counted once: a page of a thousand rows\n // under one parent is not a million steps.\n const siblingsOf = new Map<string | null, number>()\n const positionOf = new Map<string, number>()\n for (const row of rows) {\n const parent = row.parent !== null && byId.has(row.parent) ? row.parent : null\n const count = (siblingsOf.get(parent) ?? 0) + 1\n siblingsOf.set(parent, count)\n positionOf.set(row.id, count)\n }\n const levels = new Map<string, number>()\n const showing = new Map<string, boolean>()\n const result: Array<Shown> = []\n for (const row of rows) {\n const parent = row.parent !== null && byId.has(row.parent) ? row.parent : null\n const level = parent === null ? 1 : (levels.get(parent) ?? 0) + 1\n levels.set(row.id, level)\n const visible = parent === null || (showing.get(parent) === true && isOpen(model, args, parent))\n showing.set(row.id, visible)\n if (!visible) continue\n result.push({\n ...row,\n level,\n position: positionOf.get(row.id) ?? 1,\n siblings: siblingsOf.get(parent) ?? 1,\n })\n }\n return result\n}\n\n/** What a key does: move to a row, or open or close one. */\nexport type Action =\n | { readonly _tag: 'Focus'; readonly id: string }\n | { readonly _tag: 'Open'; readonly id: string }\n | { readonly _tag: 'Close'; readonly id: string }\n\nconst isPlain = (modifiers: KeyboardModifiers): boolean =>\n !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey\n\n/**\n * What a key does from the current row, over the rows that are showing, or\n * `undefined` when it is not a key the tree handles. Disabled rows are stepped\n * over. With nothing current, Down and Home land on the first row, Up and End\n * on the last. Right and Left swap under `rtl`.\n */\nexport const move = (\n rows: ReadonlyArray<Shown>,\n current: string | null,\n key: string,\n modifiers: KeyboardModifiers,\n model: Model,\n args: Args,\n direction: 'ltr' | 'rtl' = 'ltr',\n): Action | undefined => {\n const enabled = rows.filter(row => row.disabled !== true)\n if (enabled.length === 0 || !isPlain(modifiers)) return undefined\n const focus = (row: Row | undefined): Action | undefined =>\n row === undefined ? undefined : { _tag: 'Focus', id: row.id }\n const at = enabled.findIndex(row => row.id === current)\n const inward = direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n const outward = direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n if (key === 'Home') return focus(enabled[0])\n if (key === 'End') return focus(enabled[enabled.length - 1])\n if (key === 'ArrowDown')\n return focus(at === -1 ? enabled[0] : enabled[Math.min(at + 1, enabled.length - 1)])\n if (key === 'ArrowUp')\n return focus(at === -1 ? enabled[enabled.length - 1] : enabled[Math.max(at - 1, 0)])\n const row = enabled[at]\n if (row === undefined) return key === inward || key === outward ? focus(enabled[0]) : undefined\n if (key === inward) {\n if (!row.branch) return undefined\n if (!isOpen(model, args, row.id)) return { _tag: 'Open', id: row.id }\n return focus(enabled.find(child => child.parent === row.id))\n }\n if (key === outward) {\n if (row.branch && isOpen(model, args, row.id)) return { _tag: 'Close', id: row.id }\n return focus(enabled.find(parent => parent.id === row.parent))\n }\n return undefined\n}\n\n/** The row that holds the tab stop: the current one while it is showing, else the first. */\nexport const tabStop = (rows: ReadonlyArray<Shown>, current: string | null): string | undefined => {\n const here = rows.find(row => row.id === current && row.disabled !== true)\n return (here ?? rows.find(row => row.disabled !== true))?.id\n}\n\nexport interface BehaviorOptions<Input, Slots> {\n /** The slot that receives the keys: the element with `role=\"tree\"`. */\n readonly container: keyof Slots & string\n /** The slot rendered once per showing row, with `item.id` or `item.index` into `shown`. */\n readonly item: keyof Slots & string\n /** Every row, open or not, in tree order. */\n readonly rows: (input: Input) => ReadonlyArray<Row>\n /** The DOM id a row's element carries. Default: the row's own id. */\n readonly domId?: (id: string) => string\n readonly direction?: (input: Input) => 'ltr' | 'rtl'\n}\n\n/** The rows showing for one input, with what each row's attributes look up. */\ninterface Prepared {\n readonly model: Model\n readonly rows: ReadonlyArray<Shown>\n readonly byId: ReadonlyMap<string, Shown>\n readonly stop: string | undefined\n}\n\n/**\n * Wires a placed `TreeNavigation` to the slots. The container's keys move focus\n * to the row they pick, or open or close the current row with focus left where\n * it is. Each showing row gets its id, `role=\"treeitem\"`, `aria-level` (and\n * the same level as `--fk-tree-level`, so one `calc` indents any depth),\n * `aria-posinset`, `aria-setsize`, `aria-expanded` when it has children,\n * `aria-disabled` when disabled, a roving `tabindex`, and `OnFocus` reporting\n * it current. Every handled key is default-prevented.\n */\n\nexport const behavior =\n <Field extends string>(declared: Declared<typeof bundle, Field>, args: Args) =>\n <Slots>(slots: Slots) =>\n <Input extends { readonly [K in Field]: Model }, ParentMessage>(\n options: BehaviorOptions<Input, Slots>,\n ): Behavior.NamedBehavior<Slots, Input, ParentMessage> => {\n const wrap = (message: Message): ParentMessage =>\n declared.wrapper.make(message) as unknown as ParentMessage\n const slice = (input: Input): Model => input[declared.field]\n const domId = options.domId ?? ((id: string) => id)\n // Every row's attributes read the same rows: work them out once per input,\n // or a tree of n rows costs n times its whole.\n const prepare = perInput((input: Input): Prepared => {\n const model = slice(input)\n const rows = shown(options.rows(input), model, args)\n return {\n model,\n rows,\n byId: new Map(rows.map(row => [row.id, row])),\n stop: tabStop(rows, model.current),\n }\n })\n return Behavior.forSlots(slots)<Input, ParentMessage>(\n {\n [options.container]: Behavior.slot({\n requires: { capability: Capability.Interactive },\n // Focus in the tree stays on its stop when an edit elsewhere moves or removes it.\n mount: () => FollowTabStop(),\n attributes: ({\n input,\n h,\n }: {\n readonly input: Input\n readonly h: HtmlBuilder<ParentMessage>\n }) => {\n const { model, rows } = prepare(input)\n const direction = options.direction?.(input) ?? 'ltr'\n return [\n h.OnKeyDownFocus((key, modifiers) => {\n const action = move(rows, model.current, key, modifiers, model, args, direction)\n if (action === undefined) return Option.none()\n // Opening or closing keeps focus on the row it acted on.\n const message =\n action._tag === 'Focus'\n ? Message.Focused({ id: action.id })\n : action._tag === 'Open'\n ? Message.Opened({ id: action.id })\n : Message.Closed({ id: action.id })\n return Option.some({\n focusSelector: idSelector(domId(action.id)),\n message: wrap(message),\n })\n }),\n ]\n },\n }),\n [options.item]: Behavior.slot({\n requires: { capability: Capability.Focusable },\n attributes: ({\n input,\n h,\n item,\n }: {\n readonly input: Input\n readonly h: HtmlBuilder<ParentMessage>\n readonly item?: SlotItem\n }) => {\n if (item === undefined) return []\n const { model, rows, byId, stop } = prepare(input)\n const row = item.id === undefined ? rows[item.index] : byId.get(item.id)\n if (row === undefined) return []\n return [\n h.Id(domId(row.id)),\n h.Role('treeitem'),\n h.AriaLevel(row.level),\n h.Style({ '--fk-tree-level': String(row.level) }),\n h.AriaPosinset(row.position),\n h.AriaSetsize(row.siblings),\n ...(row.branch ? [h.AriaExpanded(isOpen(model, args, row.id))] : []),\n ...(row.disabled === true ? [h.AriaDisabled(true)] : []),\n h.Tabindex(stop === row.id ? 0 : -1),\n h.OnFocus(wrap(Message.Focused({ id: row.id }))),\n ]\n },\n }),\n // Keyed by values the caller chose; `forSlots` checks both keys exist.\n } as unknown as Behavior.BehaviorSpec<Slots, Input, ParentMessage>,\n { name: 'TreeNavigation' },\n )\n }\n"],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { Schema } from "effect";
2
+ import { Bundle, Declared } from "foldkit-bundle";
3
+ import { Behavior, Behaviors } from "foldkit-mixins";
4
+ import { KeyboardModifiers } from "foldkit/html";
5
+ export declare const Model: Schema.Struct<{
6
+ readonly query: Schema.String;
7
+ /** Bumped per keystroke, carried by the clear it schedules, so a superseded
8
+ * timer's `Expired` changes nothing. */
9
+ readonly generation: Schema.Number;
10
+ }>;
11
+ export type Model = typeof Model.Type;
12
+ export declare const Message: import("foldkit/message").MessageUnion<{
13
+ /** A printable key was pressed with no ctrl, alt or meta. */
14
+ readonly Typed: {
15
+ char: Schema.String;
16
+ };
17
+ /** The clear timer fired for `generation`. */
18
+ readonly Expired: {
19
+ generation: Schema.Number;
20
+ };
21
+ /** The query was dropped on purpose (a blur, a selection). */
22
+ readonly Cleared: {};
23
+ }>;
24
+ export type Message = typeof Message.Type;
25
+ export declare const Args: Schema.Struct<{
26
+ /** How long the query survives after the last key. */
27
+ readonly timeoutMs: Schema.Number;
28
+ }>;
29
+ export type Args = typeof Args.Type;
30
+ export declare const bundle: Bundle.Bundle<"Typeahead", {
31
+ readonly timeoutMs: number;
32
+ }, {
33
+ readonly query: string;
34
+ readonly generation: number;
35
+ }, {
36
+ readonly _tag: "Cleared";
37
+ } | {
38
+ readonly _tag: "Expired";
39
+ readonly generation: number;
40
+ } | {
41
+ readonly _tag: "Typed";
42
+ readonly char: string;
43
+ }, never, never, never, void, {}, {}>;
44
+ /**
45
+ * The enabled index whose text starts with `query`, searching forward from
46
+ * `current` and wrapping, or `undefined`. A query of one character, or one
47
+ * character repeated (`aaa`), starts *after* the current item so repeated
48
+ * presses cycle through the items that begin with it; a longer query starts
49
+ * *at* the current item, since the user is refining a match. Case and leading
50
+ * whitespace are ignored.
51
+ */
52
+ export declare const match: (texts: ReadonlyArray<string>, enabled: ReadonlyArray<number>, query: string, current: number) => number | undefined;
53
+ /** A single character with no ctrl, alt or meta: a typeahead key. */
54
+ export declare const isPrintable: (key: string, modifiers: KeyboardModifiers) => boolean;
55
+ export interface BehaviorOptions<Input, Slots> {
56
+ /** The slot that receives the keys. */
57
+ readonly host: keyof Slots & string;
58
+ readonly items: (input: Input) => Behaviors.Collection.Items<unknown>;
59
+ /** The text an item is matched by. */
60
+ readonly text: (input: Input, index: number) => string;
61
+ /** The current item's id, so the search starts from it; a placed
62
+ * `RovingTabindex` is the usual source. */
63
+ readonly current: (input: Input) => string | null;
64
+ }
65
+ /**
66
+ * Wires a placed `Typeahead` to the host slot: a printable key extends the
67
+ * query, focuses the matching item by its id (which then reports itself
68
+ * current, through `RovingTabindex`'s `OnFocus`), and dispatches `Typed`.
69
+ * With no match the key still extends the query, so the next one can match;
70
+ * a space with an empty query is left to the host (it activates).
71
+ */
72
+ export declare const behavior: <Field extends string>(declared: Declared<typeof bundle, Field>) => <Slots>(slots: Slots) => <Input extends { readonly [K in Field]: Model; }, ParentMessage>(options: BehaviorOptions<Input, Slots>) => Behavior.NamedBehavior<Slots, Input, ParentMessage>;
73
+ //# sourceMappingURL=typeahead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeahead.d.ts","names":[],"sources":["../../src/interaction/typeahead.ts"],"mappings":";;;;qBAea,OAAK,OAAA;;;;;;YAMN,eAAe,MAAM;qBAEpB,mCAAO;;;IAET,MAAI,OAAA;;;;IAEF,YAAU,OAAA;;;;;YAIX,iBAAiB,QAAQ;qBAExB,MAAI,OAAA;;;;YAOL,cAAc,KAAK;qBAElB,QAAM,OAAA;;;;;;;;;;;;;;;;;;;;;;qBAuCN,QAAK,OACT,uBAAqB,SACnB,uBAAqB,eACjB;;qBAgBF,cAAW,aAAe,WAAa;iBAGnC,gBAAgB,OAAO;;WAE7B,YAAY;WACZ,QAAQ,OAAO,UAAU,UAAU,WAAW;;WAE9C,OAAO,OAAO,OAAO;;;WAGrB,UAAU,OAAO;;;;;;;;;qBAUf,WACV,sBAAoB,UAAY,gBAAgB,QAAQ,YACxD,OAAK,OAAS,WACd,0BAA0B,KAAK,QAAQ,UAAS,eAAa,SACnD,gBAAgB,OAAO,WAC/B,SAAS,cAAc,OAAO,OAAO"}
@@ -0,0 +1,113 @@
1
+ import { Effect, Option, Schema } from "effect";
2
+ import { defineMessageUnion } from "foldkit/message";
3
+ import { Bundle } from "foldkit-bundle";
4
+ import { clearQuery } from "../internal.js";
5
+ import { Behavior, Capability } from "foldkit-mixins";
6
+ import { idSelector } from "./roving-tabindex.js";
7
+ // src/interaction/typeahead.ts
8
+ /**
9
+ * Typeahead over a set of items: printable keys build a query that a timer
10
+ * clears, and the query picks the item to move to. The query lives in the
11
+ * Model so a replay sees the same picks; the timer runs on Effect's clock so
12
+ * TestClock drives it. Which item matches is a pure function.
13
+ */
14
+ const Model = Schema.Struct({
15
+ query: Schema.String,
16
+ /** Bumped per keystroke, carried by the clear it schedules, so a superseded
17
+ * timer's `Expired` changes nothing. */
18
+ generation: Schema.Number
19
+ });
20
+ const Message = defineMessageUnion({
21
+ /** A printable key was pressed with no ctrl, alt or meta. */
22
+ Typed: { char: Schema.String },
23
+ /** The clear timer fired for `generation`. */
24
+ Expired: { generation: Schema.Number },
25
+ /** The query was dropped on purpose (a blur, a selection). */
26
+ Cleared: {}
27
+ });
28
+ const Args = Schema.Struct({
29
+ /** How long the query survives after the last key. */
30
+ timeoutMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) });
31
+ const bundle = Bundle.make("Typeahead", {
32
+ Model,
33
+ Message,
34
+ args: Args,
35
+ init: () => ({ model: {
36
+ query: "",
37
+ generation: 0
38
+ } }),
39
+ update: (model, message, args) => Message.match(message, {
40
+ Typed: ({ char }) => {
41
+ const generation = model.generation + 1;
42
+ return {
43
+ model: {
44
+ query: model.query + char,
45
+ generation
46
+ },
47
+ commands: [{
48
+ name: "Typeahead.expire",
49
+ args: { generation },
50
+ effect: Effect.as(Effect.sleep(args.timeoutMs), Message.Expired({ generation }))
51
+ }]
52
+ };
53
+ },
54
+ Expired: ({ generation }) => generation === model.generation ? clearQuery(model) : { model },
55
+ Cleared: () => clearQuery(model)
56
+ })
57
+ });
58
+ const normalize = (text) => text.trimStart().toLocaleLowerCase();
59
+ const isRepeated = (query) => query.length > 1 && [...query].every((char) => char === query[0]);
60
+ /**
61
+ * The enabled index whose text starts with `query`, searching forward from
62
+ * `current` and wrapping, or `undefined`. A query of one character, or one
63
+ * character repeated (`aaa`), starts *after* the current item so repeated
64
+ * presses cycle through the items that begin with it; a longer query starts
65
+ * *at* the current item, since the user is refining a match. Case and leading
66
+ * whitespace are ignored.
67
+ */
68
+ const match = (texts, enabled, query, current) => {
69
+ if (query === "" || enabled.length === 0) return void 0;
70
+ const needle = normalize(isRepeated(query) ? query[0] : query);
71
+ const position = enabled.indexOf(current);
72
+ const startAfter = needle.length === 1 || isRepeated(query);
73
+ const start = position === -1 ? 0 : startAfter ? position + 1 : position;
74
+ for (let offset = 0; offset < enabled.length; offset += 1) {
75
+ const index = enabled[(start + offset) % enabled.length];
76
+ if (normalize(texts[index] ?? "").startsWith(needle)) return index;
77
+ }
78
+ };
79
+ /** A single character with no ctrl, alt or meta: a typeahead key. */
80
+ const isPrintable = (key, modifiers) => key.length === 1 && !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey;
81
+ /**
82
+ * Wires a placed `Typeahead` to the host slot: a printable key extends the
83
+ * query, focuses the matching item by its id (which then reports itself
84
+ * current, through `RovingTabindex`'s `OnFocus`), and dispatches `Typed`.
85
+ * With no match the key still extends the query, so the next one can match;
86
+ * a space with an empty query is left to the host (it activates).
87
+ */
88
+ const behavior = (declared) => (slots) => (options) => {
89
+ const wrap = (char) => declared.wrapper.make(Message.Typed({ char }));
90
+ return Behavior.forSlots(slots)({ [options.host]: Behavior.slot({
91
+ requires: { capability: Capability.Interactive },
92
+ attributes: ({ input, h }) => {
93
+ const items = options.items(input);
94
+ const query = input[declared.field].query;
95
+ const currentId = options.current(input);
96
+ const current = currentId === null ? -1 : items.indexOf(currentId);
97
+ return [h.OnKeyDownFocus((key, modifiers) => {
98
+ if (!isPrintable(key, modifiers)) return Option.none();
99
+ if (key === " " && query === "") return Option.none();
100
+ const texts = items.ids.map((_, index) => options.text(input, index));
101
+ const found = match(texts, items.enabled, query + key, current);
102
+ const target = found === void 0 ? currentId : items.ids[found];
103
+ return Option.some({
104
+ focusSelector: target === null || target === void 0 ? ":not(*)" : idSelector(target),
105
+ message: wrap(key)
106
+ });
107
+ })];
108
+ }
109
+ }) }, { name: "Typeahead" });
110
+ };
111
+ export { Args, Message, Model, behavior, bundle, isPrintable, match };
112
+
113
+ //# sourceMappingURL=typeahead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeahead.js","names":[],"sources":["../../src/interaction/typeahead.ts"],"sourcesContent":["/**\n * Typeahead over a set of items: printable keys build a query that a timer\n * clears, and the query picks the item to move to. The query lives in the\n * Model so a replay sees the same picks; the timer runs on Effect's clock so\n * TestClock drives it. Which item matches is a pure function.\n */\nimport { Effect, Option, Schema } from 'effect'\nimport type { HtmlBuilder, KeyboardModifiers } from 'foldkit/html'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle, type Declared } from 'foldkit-bundle'\nimport { Behavior, Behaviors, Capability } from 'foldkit-mixins'\nimport { idSelector } from './roving-tabindex.js'\nimport { clearQuery } from '../internal.js'\n\nexport const Model = Schema.Struct({\n query: Schema.String,\n /** Bumped per keystroke, carried by the clear it schedules, so a superseded\n * timer's `Expired` changes nothing. */\n generation: Schema.Number,\n})\nexport type Model = typeof Model.Type\n\nexport const Message = defineMessageUnion({\n /** A printable key was pressed with no ctrl, alt or meta. */\n Typed: { char: Schema.String },\n /** The clear timer fired for `generation`. */\n Expired: { generation: Schema.Number },\n /** The query was dropped on purpose (a blur, a selection). */\n Cleared: {},\n})\nexport type Message = typeof Message.Type\n\nexport const Args = Schema.Struct({\n /** How long the query survives after the last key. */\n timeoutMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n})\nexport type Args = typeof Args.Type\n\nexport const bundle = Bundle.make('Typeahead', {\n Model,\n Message,\n args: Args,\n init: () => ({ model: { query: '', generation: 0 } }),\n update: (model, message, args) =>\n Message.match<Update.Return<Model, Message>>(message, {\n Typed: ({ char }) => {\n const generation = model.generation + 1\n return {\n model: { query: model.query + char, generation },\n commands: [\n {\n name: 'Typeahead.expire',\n args: { generation },\n effect: Effect.as(Effect.sleep(args.timeoutMs), Message.Expired({ generation })),\n },\n ],\n }\n },\n Expired: ({ generation }) =>\n generation === model.generation ? clearQuery(model) : { model },\n Cleared: () => clearQuery(model),\n }),\n})\n\nconst normalize = (text: string): string => text.trimStart().toLocaleLowerCase()\n\nconst isRepeated = (query: string): boolean =>\n query.length > 1 && [...query].every(char => char === query[0])\n\n/**\n * The enabled index whose text starts with `query`, searching forward from\n * `current` and wrapping, or `undefined`. A query of one character, or one\n * character repeated (`aaa`), starts *after* the current item so repeated\n * presses cycle through the items that begin with it; a longer query starts\n * *at* the current item, since the user is refining a match. Case and leading\n * whitespace are ignored.\n */\nexport const match = (\n texts: ReadonlyArray<string>,\n enabled: ReadonlyArray<number>,\n query: string,\n current: number,\n): number | undefined => {\n if (query === '' || enabled.length === 0) return undefined\n const needle = normalize(isRepeated(query) ? query[0]! : query)\n const position = enabled.indexOf(current)\n const startAfter = needle.length === 1 || isRepeated(query)\n const start = position === -1 ? 0 : startAfter ? position + 1 : position\n for (let offset = 0; offset < enabled.length; offset += 1) {\n const index = enabled[(start + offset) % enabled.length]!\n if (normalize(texts[index] ?? '').startsWith(needle)) return index\n }\n return undefined\n}\n\n/** A single character with no ctrl, alt or meta: a typeahead key. */\nexport const isPrintable = (key: string, modifiers: KeyboardModifiers): boolean =>\n key.length === 1 && !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey\n\nexport interface BehaviorOptions<Input, Slots> {\n /** The slot that receives the keys. */\n readonly host: keyof Slots & string\n readonly items: (input: Input) => Behaviors.Collection.Items<unknown>\n /** The text an item is matched by. */\n readonly text: (input: Input, index: number) => string\n /** The current item's id, so the search starts from it; a placed\n * `RovingTabindex` is the usual source. */\n readonly current: (input: Input) => string | null\n}\n\n/**\n * Wires a placed `Typeahead` to the host slot: a printable key extends the\n * query, focuses the matching item by its id (which then reports itself\n * current, through `RovingTabindex`'s `OnFocus`), and dispatches `Typed`.\n * With no match the key still extends the query, so the next one can match;\n * a space with an empty query is left to the host (it activates).\n */\nexport const behavior =\n <Field extends string>(declared: Declared<typeof bundle, Field>) =>\n <Slots>(slots: Slots) =>\n <Input extends { readonly [K in Field]: Model }, ParentMessage>(\n options: BehaviorOptions<Input, Slots>,\n ): Behavior.NamedBehavior<Slots, Input, ParentMessage> => {\n const wrap = (char: string): ParentMessage =>\n declared.wrapper.make(Message.Typed({ char })) as unknown as ParentMessage\n return Behavior.forSlots(slots)<Input, ParentMessage>(\n {\n [options.host]: Behavior.slot({\n requires: { capability: Capability.Interactive },\n attributes: ({\n input,\n h,\n }: {\n readonly input: Input\n readonly h: HtmlBuilder<ParentMessage>\n }) => {\n const items = options.items(input)\n const query = input[declared.field].query\n const currentId = options.current(input)\n const current = currentId === null ? -1 : items.indexOf(currentId)\n return [\n h.OnKeyDownFocus((key, modifiers) => {\n if (!isPrintable(key, modifiers)) return Option.none()\n if (key === ' ' && query === '') return Option.none()\n // Read on a key, not on every render.\n const texts = items.ids.map((_, index) => options.text(input, index))\n const found = match(texts, items.enabled, query + key, current)\n const target = found === undefined ? currentId : items.ids[found]\n return Option.some({\n // `:not(*)` matches nothing: no match and nothing current\n // moves no focus, but the key is still recorded.\n focusSelector:\n target === null || target === undefined ? ':not(*)' : idSelector(target),\n message: wrap(key),\n })\n }),\n ]\n },\n }),\n // Keyed by a value the caller chose; `forSlots` checks the key exists.\n } as unknown as Behavior.BehaviorSpec<Slots, Input, ParentMessage>,\n { name: 'Typeahead' },\n )\n }\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /** Whether two id lists hold the same ids in the same order. */
2
+ export declare const sameIds: (a: ReadonlyArray<string>, b: ReadonlyArray<string>) => boolean;
3
+ /**
4
+ * Memoizes `derive` by the identity of its input. Every slot a Behavior writes
5
+ * in one render reads the same input, so a derivation each item needs runs
6
+ * once per render rather than once per item.
7
+ */
8
+ export declare const perInput: <Input extends object, Value>(derive: (input: Input) => Value) => ((input: Input) => Value);
9
+ /** Clears a typeahead query; blur and selection usually find it empty already. */
10
+ export declare const clearQuery: <Model extends {
11
+ readonly query: string;
12
+ }>(model: Model) => {
13
+ readonly model: Model;
14
+ };
15
+ export declare const unlessSame: <Model extends object>(model: Model, next: NoInfer<Model>) => Model;
16
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","names":[],"sources":["../src/internal.ts"],"mappings":";qBAGa,UAAO,GAAO,uBAAqB,GAAK;;;;;;qBAQxC,WAAY,sBAAsB,OAAK,SACzC,OAAO,UAAU,YACvB,OAAO,UAAU;;qBAWT,aAAc;WAAyB;GAAe,OAC1D;WACK,OAAO;;qBAaR,aAAc,sBAAoB,OAAS,OAAK,MAAQ,QAAQ,WAAS"}
@@ -1,4 +1,5 @@
1
- //#region src/internal.ts
1
+ import { Equal, Option } from "effect";
2
+ // src/internal.ts
2
3
  /** Whether two id lists hold the same ids in the same order. */
3
4
  const sameIds = (a, b) => a.length === b.length && a.every((id, index) => id === b[index]);
4
5
  /**
@@ -22,16 +23,16 @@ const clearQuery = (model) => model.query === "" ? { model } : { model: {
22
23
  } };
23
24
  /**
24
25
  * `next`, or `model` itself when both hold the same fields with the same
25
- * values. Foldkit renders only when the root Model changes identity, so an
26
- * equal copy renders the whole page for nothing.
26
+ * values, an `Option` by what it holds. Foldkit renders only when the root
27
+ * Model changes identity, so an equal copy renders the whole page for nothing.
27
28
  */
29
+ const same = (left, right) => Object.is(left, right) || Option.isOption(left) && Option.isOption(right) && Equal.equals(left, right);
28
30
  const unlessSame = (model, next) => {
29
31
  const keys = Object.keys(next);
30
32
  if (keys.length !== Object.keys(model).length) return next;
31
- for (const key of keys) if (!Object.hasOwn(model, key) || !Object.is(next[key], model[key])) return next;
33
+ for (const key of keys) if (!Object.hasOwn(model, key) || !same(next[key], model[key])) return next;
32
34
  return model;
33
35
  };
34
- //#endregion
35
- export { unlessSame as i, perInput as n, sameIds as r, clearQuery as t };
36
+ export { clearQuery, perInput, sameIds, unlessSame };
36
37
 
37
- //# sourceMappingURL=internal-DP-xLKbT.mjs.map
38
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.js","names":[],"sources":["../src/internal.ts"],"sourcesContent":["import { Equal, Option } from 'effect'\n\n/** Whether two id lists hold the same ids in the same order. */\nexport const sameIds = (a: ReadonlyArray<string>, b: ReadonlyArray<string>): boolean =>\n a.length === b.length && a.every((id, index) => id === b[index])\n\n/**\n * Memoizes `derive` by the identity of its input. Every slot a Behavior writes\n * in one render reads the same input, so a derivation each item needs runs\n * once per render rather than once per item.\n */\nexport const perInput = <Input extends object, Value>(\n derive: (input: Input) => Value,\n): ((input: Input) => Value) => {\n const cache = new WeakMap<Input, Value>()\n return input => {\n if (cache.has(input)) return cache.get(input)!\n const value = derive(input)\n cache.set(input, value)\n return value\n }\n}\n\n/** Clears a typeahead query; blur and selection usually find it empty already. */\nexport const clearQuery = <Model extends { readonly query: string }>(\n model: Model,\n): { readonly model: Model } =>\n model.query === '' ? { model } : { model: { ...model, query: '' } }\n\n/**\n * `next`, or `model` itself when both hold the same fields with the same\n * values, an `Option` by what it holds. Foldkit renders only when the root\n * Model changes identity, so an equal copy renders the whole page for nothing.\n */\n// A fresh `Option.some(x)` is a new object each time, though it holds the same `x`.\nconst same = (left: unknown, right: unknown): boolean =>\n Object.is(left, right) ||\n (Option.isOption(left) && Option.isOption(right) && Equal.equals(left, right))\n\nexport const unlessSame = <Model extends object>(model: Model, next: NoInfer<Model>): Model => {\n const keys = Object.keys(next) as Array<keyof Model>\n if (keys.length !== Object.keys(model).length) return next\n for (const key of keys) {\n if (!Object.hasOwn(model, key) || !same(next[key], model[key])) return next\n }\n return model\n}\n"],"mappings":";;;AAGA,MAAa,WAAW,GAA0B,MAChD,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,IAAI,UAAU,OAAO,EAAE,MAAM;;;;;;AAOjE,MAAa,YACX,WAC8B;CAC9B,MAAM,wBAAQ,IAAI,QAAsB;CACxC,QAAO,UAAS;EACd,IAAI,MAAM,IAAI,KAAK,GAAG,OAAO,MAAM,IAAI,KAAK;EAC5C,MAAM,QAAQ,OAAO,KAAK;EAC1B,MAAM,IAAI,OAAO,KAAK;EACtB,OAAO;CACT;AACF;;AAGA,MAAa,cACX,UAEA,MAAM,UAAU,KAAK,EAAE,MAAM,IAAI,EAAE,OAAO;CAAE,GAAG;CAAO,OAAO;AAAG,EAAE;;;;;;AAQpE,MAAM,QAAQ,MAAe,UAC3B,OAAO,GAAG,MAAM,KAAK,KACpB,OAAO,SAAS,IAAI,KAAK,OAAO,SAAS,KAAK,KAAK,MAAM,OAAO,MAAM,KAAK;AAE9E,MAAa,cAAoC,OAAc,SAAgC;CAC7F,MAAM,OAAO,OAAO,KAAK,IAAI;CAC7B,IAAI,KAAK,WAAW,OAAO,KAAK,KAAK,CAAC,CAAC,QAAQ,OAAO;CACtD,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,OAAO,OAAO,OAAO,GAAG,KAAK,CAAC,KAAK,KAAK,MAAM,MAAM,IAAI,GAAG,OAAO;CAEzE,OAAO;AACT"}
@@ -0,0 +1,37 @@
1
+ import { Schema } from "effect";
2
+ import { Bundle } from "foldkit-bundle";
3
+ export declare const BreakpointsModel: Schema.Struct<{
4
+ readonly width: Schema.Number;
5
+ readonly breakpoint: Schema.NullOr<Schema.String>;
6
+ }>;
7
+ export type BreakpointsModel = typeof BreakpointsModel.Type;
8
+ export declare const BreakpointsMessage: import("foldkit/message").MessageUnion<{
9
+ readonly Changed: {
10
+ width: Schema.Number;
11
+ };
12
+ }>;
13
+ export type BreakpointsMessage = typeof BreakpointsMessage.Type;
14
+ /**
15
+ * The breakpoint a width falls in: the name with the largest threshold at or
16
+ * below it, or `null` below every threshold. `Breakpoints` uses it; a view that
17
+ * has a width of its own can too.
18
+ */
19
+ export declare const breakpointFor: (width: number, breakpoints: Readonly<Record<string, number>>) => string | null;
20
+ /**
21
+ * Named breakpoints over the viewport width: one `resize` listener, derived
22
+ * names in `update`. Thresholds are mobile-first min-widths: the breakpoint
23
+ * is the largest name whose threshold is <= width. Ties break
24
+ * alphabetically so duplicates stay deterministic.
25
+ */
26
+ export declare const Breakpoints: Bundle.Bundle<"Breakpoints", {
27
+ readonly breakpoints: {
28
+ readonly [x: string]: number;
29
+ };
30
+ }, {
31
+ readonly width: number;
32
+ readonly breakpoint: string | null;
33
+ }, {
34
+ readonly _tag: "Changed";
35
+ readonly width: number;
36
+ }, never, never, never, void, {}, {}>;
37
+ //# sourceMappingURL=breakpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breakpoints.d.ts","names":[],"sources":["../../src/media/breakpoints.ts"],"mappings":";;qBAKa,kBAAgB,OAAA;;;;YAIjB,0BAA0B,iBAAiB;qBAE1C,8CAAkB;;IAAmC,OAAK,OAAA;;;YAC3D,4BAA4B,mBAAmB;;;;;;qBAO9C,gBAAa,eACX,aACA,SAAS;;;;;;;qBA+CX,aAAW,OAAA"}
@@ -0,0 +1,64 @@
1
+ import { Effect, Queue, Schema, Stream } from "effect";
2
+ import { defineMessageUnion } from "foldkit/message";
3
+ import * as Subscription from "foldkit/subscription";
4
+ import { Bundle } from "foldkit-bundle";
5
+ // src/media/breakpoints.ts
6
+ const BreakpointsModel = Schema.Struct({
7
+ width: Schema.Number,
8
+ breakpoint: Schema.NullOr(Schema.String)
9
+ });
10
+ const BreakpointsMessage = defineMessageUnion({ Changed: { width: Schema.Number } });
11
+ /**
12
+ * The breakpoint a width falls in: the name with the largest threshold at or
13
+ * below it, or `null` below every threshold. `Breakpoints` uses it; a view that
14
+ * has a width of its own can too.
15
+ */
16
+ const breakpointFor = (width, breakpoints) => {
17
+ let best = null;
18
+ let bestThreshold = -Infinity;
19
+ for (const name of Object.keys(breakpoints)) {
20
+ const threshold = breakpoints[name];
21
+ if (threshold <= width && (threshold > bestThreshold || threshold === bestThreshold && (best === null || name > best))) {
22
+ best = name;
23
+ bestThreshold = threshold;
24
+ }
25
+ }
26
+ return best;
27
+ };
28
+ const widthStream = () => {
29
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return Stream.empty;
30
+ return Stream.unwrap(Effect.gen(function* () {
31
+ const queue = yield* Queue.unbounded();
32
+ const onResize = () => {
33
+ Effect.runFork(Queue.offer(queue, BreakpointsMessage.Changed({ width: window.innerWidth })));
34
+ };
35
+ window.addEventListener("resize", onResize);
36
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
37
+ window.removeEventListener("resize", onResize);
38
+ }));
39
+ return Stream.concat(Stream.make(BreakpointsMessage.Changed({ width: window.innerWidth })), Stream.fromQueue(queue));
40
+ }));
41
+ };
42
+ /**
43
+ * Named breakpoints over the viewport width: one `resize` listener, derived
44
+ * names in `update`. Thresholds are mobile-first min-widths: the breakpoint
45
+ * is the largest name whose threshold is <= width. Ties break
46
+ * alphabetically so duplicates stay deterministic.
47
+ */
48
+ const Breakpoints = Bundle.make("Breakpoints", {
49
+ Model: BreakpointsModel,
50
+ Message: BreakpointsMessage,
51
+ args: Schema.Struct({ breakpoints: Schema.Record(Schema.String, Schema.Number.pipe(Schema.check(Schema.isFinite()))) }),
52
+ init: (args) => ({ model: {
53
+ width: 0,
54
+ breakpoint: breakpointFor(0, args.breakpoints)
55
+ } }),
56
+ update: (model, message, args) => BreakpointsMessage.match(message, { Changed: ({ width }) => width === model.width ? { model } : { model: {
57
+ width,
58
+ breakpoint: breakpointFor(width, args.breakpoints)
59
+ } } }),
60
+ subscriptions: () => Subscription.make()(() => ({ changes: Subscription.persistentEntry(widthStream()) }))
61
+ });
62
+ export { Breakpoints, BreakpointsMessage, BreakpointsModel, breakpointFor };
63
+
64
+ //# sourceMappingURL=breakpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"breakpoints.js","names":[],"sources":["../../src/media/breakpoints.ts"],"sourcesContent":["import { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const BreakpointsModel = Schema.Struct({\n width: Schema.Number,\n breakpoint: Schema.NullOr(Schema.String),\n})\nexport type BreakpointsModel = typeof BreakpointsModel.Type\n\nexport const BreakpointsMessage = defineMessageUnion({ Changed: { width: Schema.Number } })\nexport type BreakpointsMessage = typeof BreakpointsMessage.Type\n\n/**\n * The breakpoint a width falls in: the name with the largest threshold at or\n * below it, or `null` below every threshold. `Breakpoints` uses it; a view that\n * has a width of its own can too.\n */\nexport const breakpointFor = (\n width: number,\n breakpoints: Readonly<Record<string, number>>,\n): string | null => {\n let best: string | null = null\n let bestThreshold = -Infinity\n for (const name of Object.keys(breakpoints)) {\n const threshold = breakpoints[name]!\n if (\n threshold <= width &&\n (threshold > bestThreshold || (threshold === bestThreshold && (best === null || name > best)))\n ) {\n best = name\n bestThreshold = threshold\n }\n }\n return best\n}\n\nconst widthStream = (): Stream.Stream<BreakpointsMessage> => {\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {\n return Stream.empty\n }\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<BreakpointsMessage>()\n const onResize = () => {\n Effect.runFork(Queue.offer(queue, BreakpointsMessage.Changed({ width: window.innerWidth })))\n }\n window.addEventListener('resize', onResize)\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n window.removeEventListener('resize', onResize)\n }),\n )\n return Stream.concat(\n Stream.make(BreakpointsMessage.Changed({ width: window.innerWidth })),\n Stream.fromQueue(queue),\n )\n }),\n )\n}\n\n/**\n * Named breakpoints over the viewport width: one `resize` listener, derived\n * names in `update`. Thresholds are mobile-first min-widths: the breakpoint\n * is the largest name whose threshold is <= width. Ties break\n * alphabetically so duplicates stay deterministic.\n */\nexport const Breakpoints = Bundle.make('Breakpoints', {\n Model: BreakpointsModel,\n Message: BreakpointsMessage,\n args: Schema.Struct({\n breakpoints: Schema.Record(Schema.String, Schema.Number.pipe(Schema.check(Schema.isFinite()))),\n }),\n init: args => ({\n model: { width: 0, breakpoint: breakpointFor(0, args.breakpoints) },\n }),\n update: (model, message, args) =>\n BreakpointsMessage.match(message, {\n // A height-only resize reports the same width.\n Changed: ({ width }) =>\n width === model.width\n ? { model }\n : { model: { width, breakpoint: breakpointFor(width, args.breakpoints) } },\n }),\n subscriptions: (): Subscription.Subscriptions<BreakpointsModel, BreakpointsMessage> =>\n Subscription.make<BreakpointsModel, BreakpointsMessage>()(() => ({\n changes: Subscription.persistentEntry(widthStream()),\n })),\n})\n"],"mappings":";;;;;AAKA,MAAa,mBAAmB,OAAO,OAAO;CAC5C,OAAO,OAAO;CACd,YAAY,OAAO,OAAO,OAAO,MAAM;AACzC,CAAC;AAGD,MAAa,qBAAqB,mBAAmB,EAAE,SAAS,EAAE,OAAO,OAAO,OAAO,EAAE,CAAC;;;;;;AAQ1F,MAAa,iBACX,OACA,gBACkB;CAClB,IAAI,OAAsB;CAC1B,IAAI,gBAAgB;CACpB,KAAK,MAAM,QAAQ,OAAO,KAAK,WAAW,GAAG;EAC3C,MAAM,YAAY,YAAY;EAC9B,IACE,aAAa,UACZ,YAAY,iBAAkB,cAAc,kBAAkB,SAAS,QAAQ,OAAO,QACvF;GACA,OAAO;GACP,gBAAgB;EAClB;CACF;CACA,OAAO;AACT;AAEA,MAAM,oBAAuD;CAC3D,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO,qBAAqB,YACtE,OAAO,OAAO;CAEhB,OAAO,OAAO,OACZ,OAAO,IAAI,aAAa;EACtB,MAAM,QAAQ,OAAO,MAAM,UAA8B;EACzD,MAAM,iBAAiB;GACrB,OAAO,QAAQ,MAAM,MAAM,OAAO,mBAAmB,QAAQ,EAAE,OAAO,OAAO,WAAW,CAAC,CAAC,CAAC;EAC7F;EACA,OAAO,iBAAiB,UAAU,QAAQ;EAC1C,OAAO,OAAO,mBACZ,OAAO,WAAW;GAChB,OAAO,oBAAoB,UAAU,QAAQ;EAC/C,CAAC,CACH;EACA,OAAO,OAAO,OACZ,OAAO,KAAK,mBAAmB,QAAQ,EAAE,OAAO,OAAO,WAAW,CAAC,CAAC,GACpE,OAAO,UAAU,KAAK,CACxB;CACF,CAAC,CACH;AACF;;;;;;;AAQA,MAAa,cAAc,OAAO,KAAK,eAAe;CACpD,OAAO;CACP,SAAS;CACT,MAAM,OAAO,OAAO,EAClB,aAAa,OAAO,OAAO,OAAO,QAAQ,OAAO,OAAO,KAAK,OAAO,MAAM,OAAO,SAAS,CAAC,CAAC,CAAC,EAC/F,CAAC;CACD,OAAM,UAAS,EACb,OAAO;EAAE,OAAO;EAAG,YAAY,cAAc,GAAG,KAAK,WAAW;CAAE,EACpE;CACA,SAAS,OAAO,SAAS,SACvB,mBAAmB,MAAM,SAAS,EAEhC,UAAU,EAAE,YACV,UAAU,MAAM,QACZ,EAAE,MAAM,IACR,EAAE,OAAO;EAAE;EAAO,YAAY,cAAc,OAAO,KAAK,WAAW;CAAE,EAAE,EAC/E,CAAC;CACH,qBACE,aAAa,KAA2C,CAAC,QAAQ,EAC/D,SAAS,aAAa,gBAAgB,YAAY,CAAC,EACrD,EAAE;AACN,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./breakpoints.js";
2
+ export * from "./mediaquery.js";
3
+ export * from "./platform.js";
@@ -0,0 +1,4 @@
1
+ export * from "./breakpoints.js";
2
+ export * from "./mediaquery.js";
3
+ export * from "./platform.js";
4
+ export {};
@@ -0,0 +1,40 @@
1
+ import { Schema } from "effect";
2
+ import { Bundle } from "foldkit-bundle";
3
+ export declare const MediaQueryModel: Schema.Struct<{
4
+ readonly matches: Schema.Boolean;
5
+ }>;
6
+ export type MediaQueryModel = typeof MediaQueryModel.Type;
7
+ export declare const MediaQueryMessage: import("foldkit/message").MessageUnion<{
8
+ readonly Changed: {
9
+ matches: Schema.Boolean;
10
+ };
11
+ }>;
12
+ export type MediaQueryMessage = typeof MediaQueryMessage.Type;
13
+ /**
14
+ * A media query as a bundle: `matches` in the Model, kept current by a
15
+ * `matchMedia` change stream. State stays in the parent Model, so it replays
16
+ * and time-travels like any other state; the browser only reports facts.
17
+ */
18
+ export declare const MediaQuery: Bundle.Bundle<"MediaQuery", {
19
+ readonly query: string;
20
+ }, {
21
+ readonly matches: boolean;
22
+ }, {
23
+ readonly _tag: "Changed";
24
+ readonly matches: boolean;
25
+ }, never, never, never, void, {}, {}>;
26
+ /** `(prefers-color-scheme: dark)`, bound so placements give no args. */
27
+ export declare const PrefersDark: Bundle.Bundle<"MediaQuery", void, {
28
+ readonly matches: boolean;
29
+ }, {
30
+ readonly _tag: "Changed";
31
+ readonly matches: boolean;
32
+ }, never, never, never, void, {}, {}>;
33
+ /** `(prefers-reduced-motion: reduce)`, bound so placements give no args. */
34
+ export declare const PrefersReducedMotion: Bundle.Bundle<"MediaQuery", void, {
35
+ readonly matches: boolean;
36
+ }, {
37
+ readonly _tag: "Changed";
38
+ readonly matches: boolean;
39
+ }, never, never, never, void, {}, {}>;
40
+ //# sourceMappingURL=mediaquery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediaquery.d.ts","names":[],"sources":["../../src/media/mediaquery.ts"],"mappings":";;qBAKa,iBAAe,OAAA;;;YAChB,yBAAyB,gBAAgB;qBAExC,6CAAiB;;IAAmC,SAAO,OAAA;;;YAC5D,2BAA2B,kBAAkB;;;;;;qBAiC5C,YAAU,OAAA;;;;;;;;;qBAcV,aAAW,OAAA;;;;;;;qBAGX,sBAAoB,OAAA"}
@@ -0,0 +1,42 @@
1
+ import { Effect, Queue, Schema, Stream } from "effect";
2
+ import { defineMessageUnion } from "foldkit/message";
3
+ import * as Subscription from "foldkit/subscription";
4
+ import { Bundle } from "foldkit-bundle";
5
+ // src/media/mediaquery.ts
6
+ const MediaQueryModel = Schema.Struct({ matches: Schema.Boolean });
7
+ const MediaQueryMessage = defineMessageUnion({ Changed: { matches: Schema.Boolean } });
8
+ const matchMediaStream = (query) => {
9
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return Stream.empty;
10
+ return Stream.unwrap(Effect.gen(function* () {
11
+ const queue = yield* Queue.unbounded();
12
+ const list = window.matchMedia(query);
13
+ const onChange = (event) => {
14
+ Effect.runFork(Queue.offer(queue, MediaQueryMessage.Changed({ matches: event.matches })));
15
+ };
16
+ list.addEventListener("change", onChange);
17
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
18
+ list.removeEventListener("change", onChange);
19
+ }));
20
+ return Stream.concat(Stream.make(MediaQueryMessage.Changed({ matches: list.matches })), Stream.fromQueue(queue));
21
+ }));
22
+ };
23
+ /**
24
+ * A media query as a bundle: `matches` in the Model, kept current by a
25
+ * `matchMedia` change stream. State stays in the parent Model, so it replays
26
+ * and time-travels like any other state; the browser only reports facts.
27
+ */
28
+ const MediaQuery = Bundle.make("MediaQuery", {
29
+ Model: MediaQueryModel,
30
+ Message: MediaQueryMessage,
31
+ args: Schema.Struct({ query: Schema.String }),
32
+ init: () => ({ model: { matches: false } }),
33
+ update: (model, message) => message.matches === model.matches ? { model } : { model: { matches: message.matches } },
34
+ subscriptions: ({ query }) => Subscription.make()(() => ({ changes: Subscription.persistentEntry(matchMediaStream(query)) }))
35
+ });
36
+ /** `(prefers-color-scheme: dark)`, bound so placements give no args. */
37
+ const PrefersDark = MediaQuery.with({ query: "(prefers-color-scheme: dark)" });
38
+ /** `(prefers-reduced-motion: reduce)`, bound so placements give no args. */
39
+ const PrefersReducedMotion = MediaQuery.with({ query: "(prefers-reduced-motion: reduce)" });
40
+ export { MediaQuery, MediaQueryMessage, MediaQueryModel, PrefersDark, PrefersReducedMotion };
41
+
42
+ //# sourceMappingURL=mediaquery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediaquery.js","names":[],"sources":["../../src/media/mediaquery.ts"],"sourcesContent":["import { Effect, Queue, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const MediaQueryModel = Schema.Struct({ matches: Schema.Boolean })\nexport type MediaQueryModel = typeof MediaQueryModel.Type\n\nexport const MediaQueryMessage = defineMessageUnion({ Changed: { matches: Schema.Boolean } })\nexport type MediaQueryMessage = typeof MediaQueryMessage.Type\n\nconst matchMediaStream = (query: string): Stream.Stream<MediaQueryMessage> => {\n // No window (SSR) or no matchMedia (old browser, minimal DOM): nothing to\n // listen to, so the slice keeps its initial value instead of throwing.\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return Stream.empty\n return Stream.unwrap(\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<MediaQueryMessage>()\n const list = window.matchMedia(query)\n const onChange = (event: MediaQueryListEvent) => {\n Effect.runFork(Queue.offer(queue, MediaQueryMessage.Changed({ matches: event.matches })))\n }\n list.addEventListener('change', onChange)\n yield* Effect.addFinalizer(() =>\n Effect.sync(() => {\n list.removeEventListener('change', onChange)\n }),\n )\n // The current value first, so the Model is right before the first change.\n return Stream.concat(\n Stream.make(MediaQueryMessage.Changed({ matches: list.matches })),\n Stream.fromQueue(queue),\n )\n }),\n )\n}\n\n/**\n * A media query as a bundle: `matches` in the Model, kept current by a\n * `matchMedia` change stream. State stays in the parent Model, so it replays\n * and time-travels like any other state; the browser only reports facts.\n */\nexport const MediaQuery = Bundle.make('MediaQuery', {\n Model: MediaQueryModel,\n Message: MediaQueryMessage,\n args: Schema.Struct({ query: Schema.String }),\n init: () => ({ model: { matches: false } }),\n update: (model, message) =>\n message.matches === model.matches ? { model } : { model: { matches: message.matches } },\n subscriptions: ({ query }): Subscription.Subscriptions<MediaQueryModel, MediaQueryMessage> =>\n Subscription.make<MediaQueryModel, MediaQueryMessage>()(() => ({\n changes: Subscription.persistentEntry(matchMediaStream(query)),\n })),\n})\n\n/** `(prefers-color-scheme: dark)`, bound so placements give no args. */\nexport const PrefersDark = MediaQuery.with({ query: '(prefers-color-scheme: dark)' })\n\n/** `(prefers-reduced-motion: reduce)`, bound so placements give no args. */\nexport const PrefersReducedMotion = MediaQuery.with({ query: '(prefers-reduced-motion: reduce)' })\n"],"mappings":";;;;;AAKA,MAAa,kBAAkB,OAAO,OAAO,EAAE,SAAS,OAAO,QAAQ,CAAC;AAGxE,MAAa,oBAAoB,mBAAmB,EAAE,SAAS,EAAE,SAAS,OAAO,QAAQ,EAAE,CAAC;AAG5F,MAAM,oBAAoB,UAAoD;CAG5E,IAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY,OAAO,OAAO;CAC5F,OAAO,OAAO,OACZ,OAAO,IAAI,aAAa;EACtB,MAAM,QAAQ,OAAO,MAAM,UAA6B;EACxD,MAAM,OAAO,OAAO,WAAW,KAAK;EACpC,MAAM,YAAY,UAA+B;GAC/C,OAAO,QAAQ,MAAM,MAAM,OAAO,kBAAkB,QAAQ,EAAE,SAAS,MAAM,QAAQ,CAAC,CAAC,CAAC;EAC1F;EACA,KAAK,iBAAiB,UAAU,QAAQ;EACxC,OAAO,OAAO,mBACZ,OAAO,WAAW;GAChB,KAAK,oBAAoB,UAAU,QAAQ;EAC7C,CAAC,CACH;EAEA,OAAO,OAAO,OACZ,OAAO,KAAK,kBAAkB,QAAQ,EAAE,SAAS,KAAK,QAAQ,CAAC,CAAC,GAChE,OAAO,UAAU,KAAK,CACxB;CACF,CAAC,CACH;AACF;;;;;;AAOA,MAAa,aAAa,OAAO,KAAK,cAAc;CAClD,OAAO;CACP,SAAS;CACT,MAAM,OAAO,OAAO,EAAE,OAAO,OAAO,OAAO,CAAC;CAC5C,aAAa,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE;CACzC,SAAS,OAAO,YACd,QAAQ,YAAY,MAAM,UAAU,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,SAAS,QAAQ,QAAQ,EAAE;CACxF,gBAAgB,EAAE,YAChB,aAAa,KAAyC,CAAC,QAAQ,EAC7D,SAAS,aAAa,gBAAgB,iBAAiB,KAAK,CAAC,EAC/D,EAAE;AACN,CAAC;;AAGD,MAAa,cAAc,WAAW,KAAK,EAAE,OAAO,+BAA+B,CAAC;;AAGpF,MAAa,uBAAuB,WAAW,KAAK,EAAE,OAAO,mCAAmC,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Platform detection as pure functions. `platformFromUA` reads a user-agent
3
+ * string (passed in, so tests pin it without stubbing globals); order
4
+ * matters — Android contains "Linux", iPhones mention "Mac" — so mobile
5
+ * checks come first. `isBrowser`/`isServer` split SSR from client for init
6
+ * defaults.
7
+ */
8
+ export declare const Platform: readonly ['mac', 'windows', 'linux', 'android', 'ios', 'unknown'];
9
+ export type Platform = (typeof Platform)[number];
10
+ /**
11
+ * Client Hints (`navigator.userAgentData`) plus touch points
12
+ * (`navigator.maxTouchPoints`), passed in so detection stays pure and
13
+ * pinned. Only `platform` and `touchPoints` can change the answer — the
14
+ * `mobile` flag never disambiguates on its own, so it is not accepted.
15
+ */
16
+ export interface PlatformHints {
17
+ readonly platform?: string | undefined;
18
+ readonly touchPoints?: number | undefined;
19
+ }
20
+ /**
21
+ * The platform a user-agent string names, preferring Client Hints when given
22
+ * (`navigator.userAgentData.platform`), with touch points telling a desktop-mode
23
+ * iPad from a Mac. `unknown` when nothing matches.
24
+ */
25
+ export declare const platformFromUA: (userAgent: string, hints?: PlatformHints) => Platform;
26
+ /** Whether a window exists: the client side of an SSR split. */
27
+ export declare const isBrowser: () => boolean;
28
+ /** Whether no window exists: the server side of an SSR split. */
29
+ export declare const isServer: () => boolean;
30
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","names":[],"sources":["../../src/media/platform.ts"],"mappings":";;;;;;;qBAOa;YACD,mBAAmB;;;;;;;iBAQd;WACN;WACA;;;;;;;qBAyBE,iBAAc,mBAAqB,QAAS,kBAAqB;;qBAkBjE;;qBAGA"}
@@ -0,0 +1,50 @@
1
+ // src/media/platform.ts
2
+ /**
3
+ * Platform detection as pure functions. `platformFromUA` reads a user-agent
4
+ * string (passed in, so tests pin it without stubbing globals); order
5
+ * matters — Android contains "Linux", iPhones mention "Mac" — so mobile
6
+ * checks come first. `isBrowser`/`isServer` split SSR from client for init
7
+ * defaults.
8
+ */
9
+ const Platform = [
10
+ "mac",
11
+ "windows",
12
+ "linux",
13
+ "android",
14
+ "ios",
15
+ "unknown"
16
+ ];
17
+ const hintedPlatform = (platform) => {
18
+ switch (platform.toLowerCase()) {
19
+ case "macos": return "mac";
20
+ case "windows": return "windows";
21
+ case "linux": return "linux";
22
+ case "android": return "android";
23
+ case "ios": return "ios";
24
+ default: return null;
25
+ }
26
+ };
27
+ /**
28
+ * The platform a user-agent string names, preferring Client Hints when given
29
+ * (`navigator.userAgentData.platform`), with touch points telling a desktop-mode
30
+ * iPad from a Mac. `unknown` when nothing matches.
31
+ */
32
+ const platformFromUA = (userAgent, hints = {}) => {
33
+ const hinted = hints.platform === void 0 ? null : hintedPlatform(hints.platform);
34
+ const ua = userAgent.toLowerCase();
35
+ let sniffed = "unknown";
36
+ if (ua.includes("android")) sniffed = "android";
37
+ else if (ua.includes("iphone") || ua.includes("ipad") || ua.includes("ipod")) sniffed = "ios";
38
+ else if (ua.includes("windows") || ua.includes("win32") || ua.includes("win64")) sniffed = "windows";
39
+ else if (ua.includes("macintosh") || ua.includes("mac os")) sniffed = "mac";
40
+ else if (ua.includes("linux")) sniffed = "linux";
41
+ const candidate = hinted ?? sniffed;
42
+ return candidate === "mac" && (hints.touchPoints ?? 0) > 1 ? "ios" : candidate;
43
+ };
44
+ /** Whether a window exists: the client side of an SSR split. */
45
+ const isBrowser = () => typeof window !== "undefined";
46
+ /** Whether no window exists: the server side of an SSR split. */
47
+ const isServer = () => !isBrowser();
48
+ export { Platform, isBrowser, isServer, platformFromUA };
49
+
50
+ //# sourceMappingURL=platform.js.map