@remix-run/ui 0.1.2 → 0.3.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 (318) hide show
  1. package/README.md +9 -7
  2. package/dist/animation/demos/animation.demo.d.ts +5 -0
  3. package/dist/animation/demos/animation.demo.js +92 -0
  4. package/dist/animation/demos/animation.demo.js.map +1 -0
  5. package/dist/animation/demos/aspect-ratio.d.ts +2 -0
  6. package/dist/animation/demos/aspect-ratio.js +69 -0
  7. package/dist/animation/demos/aspect-ratio.js.map +1 -0
  8. package/dist/animation/demos/bouncy-switch.d.ts +2 -0
  9. package/dist/animation/demos/bouncy-switch.js +36 -0
  10. package/dist/animation/demos/bouncy-switch.js.map +1 -0
  11. package/dist/animation/demos/color-interpolation.d.ts +1 -0
  12. package/dist/animation/demos/color-interpolation.js +53 -0
  13. package/dist/animation/demos/color-interpolation.js.map +1 -0
  14. package/dist/animation/demos/cube.d.ts +2 -0
  15. package/dist/animation/demos/cube.js +86 -0
  16. package/dist/animation/demos/cube.js.map +1 -0
  17. package/dist/animation/demos/default-animate.d.ts +2 -0
  18. package/dist/animation/demos/default-animate.js +89 -0
  19. package/dist/animation/demos/default-animate.js.map +1 -0
  20. package/dist/animation/demos/drag-release.d.ts +24 -0
  21. package/dist/animation/demos/drag-release.js +96 -0
  22. package/dist/animation/demos/drag-release.js.map +1 -0
  23. package/dist/animation/demos/enter.d.ts +1 -0
  24. package/dist/animation/demos/enter.js +19 -0
  25. package/dist/animation/demos/enter.js.map +1 -0
  26. package/dist/animation/demos/exit.d.ts +2 -0
  27. package/dist/animation/demos/exit.js +58 -0
  28. package/dist/animation/demos/exit.js.map +1 -0
  29. package/dist/animation/demos/flip-toggle.d.ts +2 -0
  30. package/dist/animation/demos/flip-toggle.js +37 -0
  31. package/dist/animation/demos/flip-toggle.js.map +1 -0
  32. package/dist/animation/demos/hold-to-confirm.d.ts +13 -0
  33. package/dist/animation/demos/hold-to-confirm.js +211 -0
  34. package/dist/animation/demos/hold-to-confirm.js.map +1 -0
  35. package/dist/animation/demos/html-content.d.ts +2 -0
  36. package/dist/animation/demos/html-content.js +30 -0
  37. package/dist/animation/demos/html-content.js.map +1 -0
  38. package/dist/animation/demos/interruptible-keyframes.d.ts +1 -0
  39. package/dist/animation/demos/interruptible-keyframes.js +48 -0
  40. package/dist/animation/demos/interruptible-keyframes.js.map +1 -0
  41. package/dist/animation/demos/keyframes.d.ts +1 -0
  42. package/dist/animation/demos/keyframes.js +24 -0
  43. package/dist/animation/demos/keyframes.js.map +1 -0
  44. package/dist/animation/demos/material-ripple.d.ts +2 -0
  45. package/dist/animation/demos/material-ripple.js +127 -0
  46. package/dist/animation/demos/material-ripple.js.map +1 -0
  47. package/dist/animation/demos/mixin-presence-list.d.ts +2 -0
  48. package/dist/animation/demos/mixin-presence-list.js +88 -0
  49. package/dist/animation/demos/mixin-presence-list.js.map +1 -0
  50. package/dist/animation/demos/mixin-reclaim.d.ts +2 -0
  51. package/dist/animation/demos/mixin-reclaim.js +90 -0
  52. package/dist/animation/demos/mixin-reclaim.js.map +1 -0
  53. package/dist/animation/demos/multi-state-badge.d.ts +2 -0
  54. package/dist/animation/demos/multi-state-badge.js +231 -0
  55. package/dist/animation/demos/multi-state-badge.js.map +1 -0
  56. package/dist/animation/demos/press.d.ts +2 -0
  57. package/dist/animation/demos/press.js +63 -0
  58. package/dist/animation/demos/press.js.map +1 -0
  59. package/dist/animation/demos/reordering.d.ts +2 -0
  60. package/dist/animation/demos/reordering.js +50 -0
  61. package/dist/animation/demos/reordering.js.map +1 -0
  62. package/dist/animation/demos/rolling-square.d.ts +2 -0
  63. package/dist/animation/demos/rolling-square.js +42 -0
  64. package/dist/animation/demos/rolling-square.js.map +1 -0
  65. package/dist/animation/demos/rotate.d.ts +1 -0
  66. package/dist/animation/demos/rotate.js +18 -0
  67. package/dist/animation/demos/rotate.js.map +1 -0
  68. package/dist/animation/demos/shared-layout.d.ts +2 -0
  69. package/dist/animation/demos/shared-layout.js +114 -0
  70. package/dist/animation/demos/shared-layout.js.map +1 -0
  71. package/dist/animation/demos/spring.demo.d.ts +6 -0
  72. package/dist/animation/demos/spring.demo.js +304 -0
  73. package/dist/animation/demos/spring.demo.js.map +1 -0
  74. package/dist/animation/demos/transition-options.d.ts +1 -0
  75. package/dist/animation/demos/transition-options.js +21 -0
  76. package/dist/animation/demos/transition-options.js.map +1 -0
  77. package/dist/animation/spring.d.ts +24 -31
  78. package/dist/animation/spring.js +31 -8
  79. package/dist/animation/spring.js.map +1 -1
  80. package/dist/components/accordion/accordion.d.ts +6 -6
  81. package/dist/components/accordion/accordion.js.map +1 -1
  82. package/dist/components/accordion/demos/card.demo.d.ts +1 -0
  83. package/dist/components/accordion/demos/card.demo.js +1 -0
  84. package/dist/components/accordion/demos/card.demo.js.map +1 -1
  85. package/dist/components/accordion/demos/multiple.demo.d.ts +1 -0
  86. package/dist/components/accordion/demos/multiple.demo.js +1 -0
  87. package/dist/components/accordion/demos/multiple.demo.js.map +1 -1
  88. package/dist/components/accordion/demos/overview.demo.d.ts +1 -0
  89. package/dist/components/accordion/demos/overview.demo.js +1 -0
  90. package/dist/components/accordion/demos/overview.demo.js.map +1 -1
  91. package/dist/components/anchor/anchor.d.ts +8 -1
  92. package/dist/components/anchor/anchor.js +30 -11
  93. package/dist/components/anchor/anchor.js.map +1 -1
  94. package/dist/components/breadcrumbs/{demos/basic.demo.d.ts → basic.demo.d.ts} +1 -0
  95. package/dist/components/breadcrumbs/{demos/basic.demo.js → basic.demo.js} +1 -0
  96. package/dist/components/breadcrumbs/basic.demo.js.map +1 -0
  97. package/dist/components/breadcrumbs/breadcrumbs.d.ts +1 -1
  98. package/dist/components/breadcrumbs/{demos/separator.demo.d.ts → separator.demo.d.ts} +1 -0
  99. package/dist/components/breadcrumbs/{demos/separator.demo.js → separator.demo.js} +1 -0
  100. package/dist/components/breadcrumbs/separator.demo.js.map +1 -0
  101. package/dist/components/button/button.d.ts +4 -4
  102. package/dist/components/button/button.js +3 -1
  103. package/dist/components/button/button.js.map +1 -1
  104. package/dist/components/button/demos/aliases.demo.d.ts +1 -0
  105. package/dist/components/button/demos/aliases.demo.js +1 -0
  106. package/dist/components/button/demos/aliases.demo.js.map +1 -1
  107. package/dist/components/button/demos/basic.demo.d.ts +1 -0
  108. package/dist/components/button/demos/basic.demo.js +1 -0
  109. package/dist/components/button/demos/basic.demo.js.map +1 -1
  110. package/dist/components/button/demos/states.demo.d.ts +1 -0
  111. package/dist/components/button/demos/states.demo.js +1 -0
  112. package/dist/components/button/demos/states.demo.js.map +1 -1
  113. package/dist/components/combobox/combobox.d.ts +10 -10
  114. package/dist/components/combobox/combobox.js.map +1 -1
  115. package/dist/components/combobox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  116. package/dist/components/combobox/{demos/overview.demo.js → overview.demo.js} +1 -0
  117. package/dist/components/combobox/overview.demo.js.map +1 -0
  118. package/dist/components/listbox/listbox.d.ts +3 -3
  119. package/dist/components/listbox/listbox.js.map +1 -1
  120. package/dist/components/listbox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  121. package/dist/components/listbox/{demos/overview.demo.js → overview.demo.js} +1 -0
  122. package/dist/components/listbox/overview.demo.js.map +1 -0
  123. package/dist/components/menu/demos/bubbling.demo.d.ts +1 -0
  124. package/dist/components/menu/demos/bubbling.demo.js +1 -0
  125. package/dist/components/menu/demos/bubbling.demo.js.map +1 -1
  126. package/dist/components/menu/demos/context-trigger.demo.d.ts +7 -0
  127. package/dist/components/menu/demos/context-trigger.demo.js +87 -0
  128. package/dist/components/menu/demos/context-trigger.demo.js.map +1 -0
  129. package/dist/components/menu/demos/overview.demo.d.ts +1 -0
  130. package/dist/components/menu/demos/overview.demo.js +1 -0
  131. package/dist/components/menu/demos/overview.demo.js.map +1 -1
  132. package/dist/components/menu/menu.d.ts +15 -12
  133. package/dist/components/menu/menu.js +55 -0
  134. package/dist/components/menu/menu.js.map +1 -1
  135. package/dist/components/popover/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  136. package/dist/components/popover/{demos/overview.demo.js → overview.demo.js} +1 -0
  137. package/dist/components/popover/overview.demo.js.map +1 -0
  138. package/dist/components/popover/popover.d.ts +7 -7
  139. package/dist/components/popover/popover.js +11 -4
  140. package/dist/components/popover/popover.js.map +1 -1
  141. package/dist/components/select/{demos/deconstructed.demo.d.ts → deconstructed.demo.d.ts} +1 -0
  142. package/dist/components/select/{demos/deconstructed.demo.js → deconstructed.demo.js} +1 -0
  143. package/dist/components/select/deconstructed.demo.js.map +1 -0
  144. package/dist/components/select/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
  145. package/dist/components/select/{demos/overview.demo.js → overview.demo.js} +1 -0
  146. package/dist/components/select/overview.demo.js.map +1 -0
  147. package/dist/components/select/select.d.ts +10 -10
  148. package/dist/components/select/select.js +1 -1
  149. package/dist/components/select/select.js.map +1 -1
  150. package/dist/components/tabs/tabs.d.ts +3 -3
  151. package/dist/index.d.ts +1 -1
  152. package/dist/index.js.map +1 -1
  153. package/dist/interactions/keydown/keydown.d.ts +1 -1
  154. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -1
  155. package/dist/interactions/typeahead/typeahead-mixin.d.ts +3 -1
  156. package/dist/runtime/client-entries.d.ts +1 -2
  157. package/dist/runtime/client-entries.js.map +1 -1
  158. package/dist/runtime/component.d.ts +20 -17
  159. package/dist/runtime/component.js +15 -6
  160. package/dist/runtime/component.js.map +1 -1
  161. package/dist/runtime/core/attributes.d.ts +3 -1
  162. package/dist/runtime/core/attributes.js +19 -1
  163. package/dist/runtime/core/attributes.js.map +1 -1
  164. package/dist/runtime/core/props.js +6 -5
  165. package/dist/runtime/core/props.js.map +1 -1
  166. package/dist/runtime/demos/basic.demo.d.ts +6 -0
  167. package/dist/runtime/demos/basic.demo.js +16 -0
  168. package/dist/runtime/demos/basic.demo.js.map +1 -0
  169. package/dist/runtime/demos/controlled-uncontrolled-values.demo.d.ts +6 -0
  170. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js +77 -0
  171. package/dist/runtime/demos/controlled-uncontrolled-values.demo.js.map +1 -0
  172. package/dist/runtime/demos/keyed-list.demo.d.ts +6 -0
  173. package/dist/runtime/demos/keyed-list.demo.js +62 -0
  174. package/dist/runtime/demos/keyed-list.demo.js.map +1 -0
  175. package/dist/runtime/demos/readme.demo.d.ts +5 -0
  176. package/dist/runtime/demos/readme.demo.js +396 -0
  177. package/dist/runtime/demos/readme.demo.js.map +1 -0
  178. package/dist/runtime/diff-dom.js +194 -21
  179. package/dist/runtime/diff-dom.js.map +1 -1
  180. package/dist/runtime/dom.d.ts +16 -9
  181. package/dist/runtime/frame.d.ts +7 -0
  182. package/dist/runtime/frame.js +176 -41
  183. package/dist/runtime/frame.js.map +1 -1
  184. package/dist/runtime/jsx.d.ts +3 -4
  185. package/dist/runtime/jsx.js.map +1 -1
  186. package/dist/runtime/mixins/link-mixin.d.ts +5 -1
  187. package/dist/runtime/mixins/link-mixin.js.map +1 -1
  188. package/dist/runtime/mixins/mixin.d.ts +2 -1
  189. package/dist/runtime/mixins/mixin.js.map +1 -1
  190. package/dist/runtime/mixins/ref-mixin.d.ts +2 -1
  191. package/dist/runtime/mixins/ref-mixin.js.map +1 -1
  192. package/dist/runtime/reconcile.d.ts +1 -1
  193. package/dist/runtime/reconcile.js +19 -5
  194. package/dist/runtime/reconcile.js.map +1 -1
  195. package/dist/runtime/scheduler.d.ts +10 -10
  196. package/dist/runtime/scheduler.js +1 -1
  197. package/dist/runtime/scheduler.js.map +1 -1
  198. package/dist/runtime/svg-attributes.js +1 -0
  199. package/dist/runtime/svg-attributes.js.map +1 -1
  200. package/dist/runtime/to-vnode.js +2 -2
  201. package/dist/runtime/to-vnode.js.map +1 -1
  202. package/dist/runtime/vnode.d.ts +6 -1
  203. package/dist/runtime/vnode.js +4 -0
  204. package/dist/runtime/vnode.js.map +1 -1
  205. package/dist/server/stream.d.ts +2 -0
  206. package/dist/server/stream.js +82 -11
  207. package/dist/server/stream.js.map +1 -1
  208. package/dist/style/css-mixin.d.ts +2 -1
  209. package/dist/style/css-mixin.js.map +1 -1
  210. package/dist/style/index.d.ts +1 -1
  211. package/dist/style/style.js +27 -10
  212. package/dist/style/style.js.map +1 -1
  213. package/dist/style/stylesheet.d.ts +13 -13
  214. package/dist/style/stylesheet.js.map +1 -1
  215. package/dist/theme/contract.d.ts +24 -222
  216. package/dist/theme/contract.js +2 -2
  217. package/dist/theme/contract.js.map +1 -1
  218. package/dist/theme/glyph-contract.d.ts +1 -3
  219. package/dist/theme/glyph-contract.js.map +1 -1
  220. package/dist/theme/presets/rmx-01/index.d.ts +4 -2
  221. package/dist/theme/presets/rmx-01/index.js.map +1 -1
  222. package/dist/utils/scroll-lock.d.ts +3 -3
  223. package/dist/utils/scroll-lock.js.map +1 -1
  224. package/package.json +4 -4
  225. package/src/animation/README.md +7 -5
  226. package/src/animation/demos/animation.demo.tsx +194 -0
  227. package/src/animation/demos/aspect-ratio.tsx +120 -0
  228. package/src/animation/demos/bouncy-switch.tsx +47 -0
  229. package/src/animation/demos/color-interpolation.tsx +73 -0
  230. package/src/animation/demos/cube.tsx +116 -0
  231. package/src/animation/demos/default-animate.tsx +129 -0
  232. package/src/animation/demos/drag-release.ts +118 -0
  233. package/src/animation/demos/enter.tsx +22 -0
  234. package/src/animation/demos/exit.tsx +79 -0
  235. package/src/animation/demos/flip-toggle.tsx +46 -0
  236. package/src/animation/demos/hold-to-confirm.tsx +317 -0
  237. package/src/animation/demos/html-content.tsx +41 -0
  238. package/src/animation/demos/interruptible-keyframes.tsx +62 -0
  239. package/src/animation/demos/keyframes.tsx +29 -0
  240. package/src/animation/demos/material-ripple.tsx +158 -0
  241. package/src/animation/demos/mixin-presence-list.tsx +121 -0
  242. package/src/animation/demos/mixin-reclaim.tsx +125 -0
  243. package/src/animation/demos/multi-state-badge.tsx +426 -0
  244. package/src/animation/demos/press.tsx +67 -0
  245. package/src/animation/demos/reordering.tsx +65 -0
  246. package/src/animation/demos/rolling-square.tsx +57 -0
  247. package/src/animation/demos/rotate.tsx +21 -0
  248. package/src/animation/demos/shared-layout.tsx +211 -0
  249. package/src/animation/demos/spring.demo.tsx +385 -0
  250. package/src/animation/demos/transition-options.tsx +24 -0
  251. package/src/animation/spring.ts +43 -9
  252. package/src/components/accordion/accordion.tsx +7 -5
  253. package/src/components/accordion/demos/card.demo.tsx +1 -0
  254. package/src/components/accordion/demos/multiple.demo.tsx +1 -0
  255. package/src/components/accordion/demos/overview.demo.tsx +1 -0
  256. package/src/components/anchor/README.md +11 -3
  257. package/src/components/anchor/anchor.ts +54 -13
  258. package/src/components/breadcrumbs/{demos/basic.demo.tsx → basic.demo.tsx} +1 -0
  259. package/src/components/breadcrumbs/breadcrumbs.tsx +1 -1
  260. package/src/components/breadcrumbs/{demos/separator.demo.tsx → separator.demo.tsx} +1 -0
  261. package/src/components/button/button.tsx +30 -18
  262. package/src/components/button/demos/aliases.demo.tsx +1 -0
  263. package/src/components/button/demos/basic.demo.tsx +1 -0
  264. package/src/components/button/demos/states.demo.tsx +1 -0
  265. package/src/components/combobox/README.md +1 -1
  266. package/src/components/combobox/combobox.tsx +30 -12
  267. package/src/components/combobox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  268. package/src/components/glyph/glyph.tsx +2 -2
  269. package/src/components/listbox/listbox.ts +88 -83
  270. package/src/components/listbox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  271. package/src/components/menu/README.md +26 -2
  272. package/src/components/menu/demos/bubbling.demo.tsx +1 -0
  273. package/src/components/menu/demos/context-trigger.demo.tsx +133 -0
  274. package/src/components/menu/demos/overview.demo.tsx +1 -0
  275. package/src/components/menu/menu.tsx +100 -16
  276. package/src/components/popover/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  277. package/src/components/popover/popover.ts +101 -81
  278. package/src/components/select/{demos/deconstructed.demo.tsx → deconstructed.demo.tsx} +1 -0
  279. package/src/components/select/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
  280. package/src/components/select/select.tsx +27 -10
  281. package/src/index.ts +1 -0
  282. package/src/runtime/client-entries.ts +1 -5
  283. package/src/runtime/component.ts +37 -15
  284. package/src/runtime/core/attributes.ts +21 -0
  285. package/src/runtime/core/props.ts +6 -4
  286. package/src/runtime/demos/basic.demo.tsx +21 -0
  287. package/src/runtime/demos/controlled-uncontrolled-values.demo.tsx +177 -0
  288. package/src/runtime/demos/keyed-list.demo.tsx +94 -0
  289. package/src/runtime/demos/readme.demo.tsx +702 -0
  290. package/src/runtime/diff-dom.ts +270 -20
  291. package/src/runtime/dom.ts +16 -9
  292. package/src/runtime/frame.ts +218 -41
  293. package/src/runtime/jsx.ts +7 -11
  294. package/src/runtime/mixins/link-mixin.ts +88 -86
  295. package/src/runtime/mixins/mixin.ts +9 -1
  296. package/src/runtime/mixins/ref-mixin.ts +4 -5
  297. package/src/runtime/reconcile.ts +23 -14
  298. package/src/runtime/scheduler.ts +20 -20
  299. package/src/runtime/svg-attributes.ts +1 -0
  300. package/src/runtime/to-vnode.ts +2 -2
  301. package/src/runtime/vnode.ts +10 -0
  302. package/src/server/README.md +4 -2
  303. package/src/server/stream.ts +84 -10
  304. package/src/style/css-mixin.ts +6 -1
  305. package/src/style/index.ts +1 -2
  306. package/src/style/style.ts +29 -10
  307. package/src/style/stylesheet.ts +14 -2
  308. package/src/theme/contract.ts +37 -5
  309. package/src/theme/glyph-contract.ts +3 -1
  310. package/src/theme/presets/rmx-01/index.ts +4 -2
  311. package/src/utils/scroll-lock.ts +7 -3
  312. package/dist/components/breadcrumbs/demos/basic.demo.js.map +0 -1
  313. package/dist/components/breadcrumbs/demos/separator.demo.js.map +0 -1
  314. package/dist/components/combobox/demos/overview.demo.js.map +0 -1
  315. package/dist/components/listbox/demos/overview.demo.js.map +0 -1
  316. package/dist/components/popover/demos/overview.demo.js.map +0 -1
  317. package/dist/components/select/demos/deconstructed.demo.js.map +0 -1
  318. package/dist/components/select/demos/overview.demo.js.map +0 -1
@@ -0,0 +1,211 @@
1
+ import { css, on, type Handle, type RemixNode } from '@remix-run/ui'
2
+ import { animateEntrance, animateExit } from '@remix-run/ui/animation'
3
+
4
+ const ease = 'cubic-bezier(0.26, 0.02, 0.23, 0.94)'
5
+
6
+ function OverlapExample(handle: Handle<{ state: boolean }>) {
7
+ let shouldAnimate = false
8
+ handle.queueTask(() => {
9
+ shouldAnimate = true
10
+ })
11
+
12
+ return () => {
13
+ let { state } = handle.props
14
+ let animationMix = [
15
+ animateExit({
16
+ opacity: 0,
17
+ transform: 'scale(0.8)',
18
+ duration: 300,
19
+ easing: ease,
20
+ }),
21
+ ]
22
+ if (shouldAnimate) {
23
+ animationMix.unshift(
24
+ animateEntrance({
25
+ opacity: 0,
26
+ transform: 'scale(0.6)',
27
+ duration: 300,
28
+ easing: ease,
29
+ }),
30
+ )
31
+ }
32
+
33
+ return (
34
+ <div
35
+ // grid layout so children render in the same position
36
+ mix={[css({ display: 'grid', width: 80, height: 80, '& > *': { gridArea: '1 / 1' } })]}
37
+ >
38
+ {state ? (
39
+ <div key="filled" mix={animationMix}>
40
+ <Circle filled>
41
+ <FilledIcon />
42
+ </Circle>
43
+ </div>
44
+ ) : (
45
+ <div key="outline" mix={animationMix}>
46
+ <Circle>
47
+ <OutlineIcon />
48
+ </Circle>
49
+ </div>
50
+ )}
51
+ </div>
52
+ )
53
+ }
54
+ }
55
+
56
+ function WaitExample(handle: Handle<{ state: boolean }>) {
57
+ let shouldAnimate = false
58
+ handle.queueTask(() => {
59
+ shouldAnimate = true
60
+ })
61
+
62
+ return () => {
63
+ let { state } = handle.props
64
+ let animationMix = [
65
+ animateExit({
66
+ opacity: 0,
67
+ transform: 'scale(0.8)',
68
+ duration: 300,
69
+ easing: ease,
70
+ }),
71
+ ]
72
+ if (shouldAnimate) {
73
+ animationMix.unshift(
74
+ animateEntrance({
75
+ opacity: 0,
76
+ transform: 'scale(0.6)',
77
+ duration: 300,
78
+ easing: ease,
79
+ delay: 300,
80
+ }),
81
+ )
82
+ }
83
+
84
+ return (
85
+ <div
86
+ // grid layout so children render in the same position
87
+ mix={[css({ display: 'grid', width: 80, height: 80, '& > *': { gridArea: '1 / 1' } })]}
88
+ >
89
+ {state ? (
90
+ <div key="filled" mix={animationMix}>
91
+ <Circle filled>
92
+ <FilledIcon />
93
+ </Circle>
94
+ </div>
95
+ ) : (
96
+ <div key="outline" mix={animationMix}>
97
+ <Circle>
98
+ <OutlineIcon />
99
+ </Circle>
100
+ </div>
101
+ )}
102
+ </div>
103
+ )
104
+ }
105
+ }
106
+
107
+ export function SharedLayout(handle: Handle) {
108
+ let state = true
109
+
110
+ return () => (
111
+ <div
112
+ mix={[
113
+ css({
114
+ display: 'flex',
115
+ flexDirection: 'column',
116
+ alignItems: 'center',
117
+ gap: 16,
118
+ }),
119
+ ]}
120
+ >
121
+ <div mix={[css({ display: 'flex', gap: 16 })]}>
122
+ <OverlapExample state={state} />
123
+ <WaitExample state={state} />
124
+ </div>
125
+ <button
126
+ mix={[
127
+ css({
128
+ backgroundColor: '#0f1115',
129
+ color: '#f5f5f5',
130
+ border: 'none',
131
+ borderRadius: 8,
132
+ padding: '12px 32px',
133
+ fontSize: 14,
134
+ fontWeight: 500,
135
+ cursor: 'pointer',
136
+ transition: 'transform 100ms ease-in-out',
137
+ '&:active': {
138
+ transform: 'scale(0.95)',
139
+ },
140
+ }),
141
+ on('click', () => {
142
+ state = !state
143
+ handle.update()
144
+ }),
145
+ ]}
146
+ >
147
+ Switch
148
+ </button>
149
+ </div>
150
+ )
151
+ }
152
+
153
+ function Circle(handle: Handle<{ filled?: boolean; children: RemixNode }>) {
154
+ return () => (
155
+ <div
156
+ mix={[
157
+ css({
158
+ width: 80,
159
+ height: 80,
160
+ borderRadius: '50%',
161
+ display: 'flex',
162
+ alignItems: 'center',
163
+ justifyContent: 'center',
164
+ boxSizing: 'border-box',
165
+ backgroundColor: handle.props.filled ? '#0f1115' : 'transparent',
166
+ color: handle.props.filled ? '#f5f5f5' : '#0f1115',
167
+ border: handle.props.filled ? '2px solid #0f1115' : '2px solid #0f1115',
168
+ }),
169
+ ]}
170
+ >
171
+ {handle.props.children}
172
+ </div>
173
+ )
174
+ }
175
+
176
+ function FilledIcon() {
177
+ return () => (
178
+ <svg
179
+ width="24"
180
+ height="24"
181
+ viewBox="0 0 24 24"
182
+ fill="none"
183
+ stroke="currentColor"
184
+ strokeWidth="2"
185
+ strokeLinecap="round"
186
+ strokeLinejoin="round"
187
+ >
188
+ <path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6" />
189
+ <path d="m21 3-9 9" />
190
+ <path d="M15 3h6v6" />
191
+ </svg>
192
+ )
193
+ }
194
+
195
+ function OutlineIcon() {
196
+ return () => (
197
+ <svg
198
+ width="24"
199
+ height="24"
200
+ viewBox="0 0 24 24"
201
+ fill="none"
202
+ stroke="currentColor"
203
+ strokeWidth="2"
204
+ strokeLinecap="round"
205
+ strokeLinejoin="round"
206
+ >
207
+ <rect x="3" y="3" width="18" height="18" rx="2" />
208
+ <path d="M9 12h6" />
209
+ </svg>
210
+ )
211
+ }
@@ -0,0 +1,385 @@
1
+ import { addEventListeners, css, on, ref, type Handle } from '@remix-run/ui'
2
+
3
+ import { dragVelocityEvents } from './drag-release.ts'
4
+ import { spring, type SpringPreset } from '@remix-run/ui/animation'
5
+
6
+ interface TrailPoint {
7
+ x: number
8
+ y: number
9
+ time: number
10
+ }
11
+
12
+ function PointerTrail(handle: Handle) {
13
+ let canvas: HTMLCanvasElement
14
+ let points: TrailPoint[] = []
15
+ let isDown = false
16
+ let releaseTime = 0
17
+ let animationId: number | null = null
18
+
19
+ let maxAge = 150 // ms - how long points stay in the trail while dragging
20
+ let fadeDuration = 800 // ms - how long the trail fades after release
21
+
22
+ function draw() {
23
+ if (!canvas) return
24
+ let ctx = canvas.getContext('2d')
25
+ if (!ctx) return
26
+
27
+ let now = performance.now()
28
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
29
+
30
+ // Calculate overall opacity based on release time
31
+ let overallOpacity = 1
32
+ if (!isDown && releaseTime > 0) {
33
+ let elapsed = now - releaseTime
34
+ overallOpacity = Math.max(0, 1 - elapsed / fadeDuration)
35
+ if (overallOpacity <= 0) {
36
+ points = []
37
+ animationId = null
38
+ return
39
+ }
40
+ }
41
+
42
+ // Filter out old points while dragging
43
+ if (isDown) {
44
+ points = points.filter((p) => now - p.time < maxAge)
45
+ }
46
+
47
+ if (points.length < 2) {
48
+ if (isDown || overallOpacity > 0) {
49
+ animationId = requestAnimationFrame(draw)
50
+ } else {
51
+ animationId = null
52
+ }
53
+ return
54
+ }
55
+
56
+ // Draw the trail as a tapered path
57
+ ctx.beginPath()
58
+ ctx.lineCap = 'round'
59
+ ctx.lineJoin = 'round'
60
+
61
+ for (let i = 1; i < points.length; i++) {
62
+ let prev = points[i - 1]
63
+ let curr = points[i]
64
+
65
+ // Age-based opacity (older = more transparent)
66
+ let age = isDown ? (now - prev.time) / maxAge : 0
67
+ let segmentOpacity = (1 - age) * overallOpacity
68
+
69
+ // Thickness tapers from thin (old) to thick (new)
70
+ let progress = i / (points.length - 1)
71
+ let thickness = 2 + progress * 8
72
+
73
+ ctx.beginPath()
74
+ ctx.moveTo(prev.x, prev.y)
75
+ ctx.lineTo(curr.x, curr.y)
76
+ ctx.strokeStyle = `rgba(14, 165, 233, ${segmentOpacity * 0.6})`
77
+ ctx.lineWidth = thickness
78
+ ctx.stroke()
79
+ }
80
+
81
+ // Draw a red glow at the end to show direction
82
+ if (points.length > 0) {
83
+ let last = points[points.length - 1]
84
+ let gradient = ctx.createRadialGradient(last.x, last.y, 0, last.x, last.y, 20)
85
+ gradient.addColorStop(0, `rgba(239, 68, 68, ${overallOpacity * 0.6})`)
86
+ gradient.addColorStop(1, 'rgba(239, 68, 68, 0)')
87
+ ctx.fillStyle = gradient
88
+ ctx.beginPath()
89
+ ctx.arc(last.x, last.y, 20, 0, Math.PI * 2)
90
+ ctx.fill()
91
+ }
92
+
93
+ animationId = requestAnimationFrame(draw)
94
+ }
95
+
96
+ function startDrawing() {
97
+ if (!animationId) {
98
+ animationId = requestAnimationFrame(draw)
99
+ }
100
+ }
101
+
102
+ if (typeof document !== 'undefined') {
103
+ addEventListeners(document, handle.signal, {
104
+ pointerdown(event) {
105
+ if (!(event.target as HTMLElement).closest('.draggable')) return
106
+ isDown = true
107
+ releaseTime = 0
108
+ points = [{ x: event.clientX, y: event.clientY, time: performance.now() }]
109
+ startDrawing()
110
+ },
111
+
112
+ pointermove(event) {
113
+ if (!isDown) return
114
+ points.push({ x: event.clientX, y: event.clientY, time: performance.now() })
115
+ },
116
+
117
+ pointerup() {
118
+ if (!isDown) return
119
+ isDown = false
120
+ releaseTime = performance.now()
121
+ },
122
+
123
+ pointercancel() {
124
+ isDown = false
125
+ releaseTime = performance.now()
126
+ },
127
+ })
128
+ }
129
+
130
+ return () => (
131
+ <canvas
132
+ mix={[
133
+ ref((node) => {
134
+ canvas = node
135
+ canvas.width = window.innerWidth
136
+ canvas.height = window.innerHeight
137
+ }),
138
+ css({
139
+ position: 'absolute',
140
+ inset: 0,
141
+ pointerEvents: 'none',
142
+ zIndex: 5,
143
+ }),
144
+ ]}
145
+ />
146
+ )
147
+ }
148
+
149
+ /**
150
+ * @name Spring Drag Release
151
+ * @description A full-screen spring physics playground with click targets and drag-release velocity.
152
+ */
153
+ export default function SpringDemo(handle: Handle) {
154
+ let viewportWidth = globalThis.window?.innerWidth ?? 1000
155
+ let viewportHeight = globalThis.window?.innerHeight ?? 700
156
+
157
+ // Target circle position (click to move)
158
+ let targetX = viewportWidth / 2
159
+ let targetY = viewportHeight / 2
160
+
161
+ // Draggable circle state
162
+ let dragX = viewportWidth / 2 - 150
163
+ let dragY = viewportHeight / 2
164
+ let isDragging = false
165
+ let isAnimating = false
166
+
167
+ // Current spring transitions (separate for X and Y to capture 2D velocity)
168
+ let transitionX = ''
169
+ let transitionY = ''
170
+
171
+ let selectedPreset: SpringPreset = 'bouncy'
172
+
173
+ // Get default spring transition for target circle
174
+ let springValue = spring(selectedPreset)
175
+
176
+ if (typeof document !== 'undefined') {
177
+ addEventListeners(document, handle.signal, {
178
+ click(event) {
179
+ // Ignore clicks on controls or when dragging
180
+ if ((event.target as HTMLElement).closest('.controls')) return
181
+ if ((event.target as HTMLElement).closest('.draggable')) return
182
+
183
+ targetX = event.clientX
184
+ targetY = event.clientY
185
+ handle.update()
186
+ },
187
+ })
188
+ }
189
+
190
+ return () => (
191
+ <div
192
+ mix={[
193
+ css({
194
+ position: 'fixed',
195
+ inset: 0,
196
+ backgroundColor: '#1a1a2e',
197
+ cursor: 'crosshair',
198
+ overflow: 'hidden',
199
+ }),
200
+ ]}
201
+ >
202
+ {/* Pointer trail */}
203
+ <PointerTrail />
204
+
205
+ {/* Target circle */}
206
+ <div
207
+ mix={[
208
+ css({
209
+ position: 'absolute',
210
+ width: '80px',
211
+ height: '80px',
212
+ borderRadius: '50%',
213
+ backgroundColor: 'transparent',
214
+ border: '3px dashed rgba(233, 69, 96, 0.5)',
215
+ transform: 'translate(-50%, -50%)',
216
+ pointerEvents: 'none',
217
+ }),
218
+ ]}
219
+ style={{
220
+ left: `${targetX}px`,
221
+ top: `${targetY}px`,
222
+ transition: `left ${springValue}, top ${springValue}`,
223
+ }}
224
+ />
225
+
226
+ {/* Draggable circle */}
227
+ <div
228
+ className="draggable"
229
+ mix={[
230
+ css({
231
+ position: 'absolute',
232
+ width: '60px',
233
+ height: '60px',
234
+ borderRadius: '50%',
235
+ backgroundColor: '#0ea5e9',
236
+ boxShadow: '0 0 20px rgba(14, 165, 233, 0.5)',
237
+ transform: 'translate(-50%, -50%)',
238
+ userSelect: 'none',
239
+ touchAction: 'none',
240
+ zIndex: 10,
241
+ }),
242
+ on('transitionend', () => {
243
+ isAnimating = false
244
+ handle.update()
245
+ }),
246
+ on('pointerdown', (event) => {
247
+ event.preventDefault()
248
+ isDragging = true
249
+ isAnimating = false
250
+ handle.update()
251
+ }),
252
+ on('pointermove', (event) => {
253
+ if (!isDragging) return
254
+ dragX = event.clientX
255
+ dragY = event.clientY
256
+ handle.update()
257
+ }),
258
+ dragVelocityEvents(),
259
+ on(dragVelocityEvents.release, (event) => {
260
+ isDragging = false
261
+
262
+ // Calculate distance to target on each axis
263
+ let distX = targetX - dragX
264
+ let distY = targetY - dragY
265
+
266
+ if (Math.abs(distX) < 1 && Math.abs(distY) < 1) {
267
+ handle.update()
268
+ return
269
+ }
270
+
271
+ // Create separate springs for X and Y with their own normalized velocities
272
+ // normalizedVelocity = velocity / distance (with sign!) for each axis
273
+ // The sign matters: positive = moving toward target, negative = moving away
274
+ if (Math.abs(distX) >= 1) {
275
+ let normalizedVelocityX = event.velocityX / distX
276
+ normalizedVelocityX = Math.max(-20, Math.min(20, normalizedVelocityX))
277
+ transitionX = String(spring(selectedPreset, { velocity: normalizedVelocityX }))
278
+ } else {
279
+ transitionX = String(spring(selectedPreset))
280
+ }
281
+
282
+ if (Math.abs(distY) >= 1) {
283
+ let normalizedVelocityY = event.velocityY / distY
284
+ normalizedVelocityY = Math.max(-20, Math.min(20, normalizedVelocityY))
285
+ transitionY = String(spring(selectedPreset, { velocity: normalizedVelocityY }))
286
+ } else {
287
+ transitionY = String(spring(selectedPreset))
288
+ }
289
+
290
+ // Animate to target
291
+ isAnimating = true
292
+ dragX = targetX
293
+ dragY = targetY
294
+ handle.update()
295
+ }),
296
+ ]}
297
+ style={{
298
+ left: `${dragX}px`,
299
+ top: `${dragY}px`,
300
+ cursor: isDragging ? 'grabbing' : 'grab',
301
+ transition: isAnimating ? `left ${transitionX}, top ${transitionY}` : 'none',
302
+ }}
303
+ />
304
+
305
+ {/* Controls */}
306
+ <div
307
+ className="controls"
308
+ mix={[
309
+ css({
310
+ position: 'absolute',
311
+ top: '20px',
312
+ left: '50%',
313
+ transform: 'translateX(-50%)',
314
+ display: 'flex',
315
+ gap: '4px',
316
+ padding: '4px',
317
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
318
+ borderRadius: '8px',
319
+ fontFamily: 'system-ui, sans-serif',
320
+ fontSize: '14px',
321
+ cursor: 'default',
322
+ }),
323
+ ]}
324
+ >
325
+ {(Object.keys(spring.presets) as SpringPreset[]).map((preset) => (
326
+ <label
327
+ key={preset}
328
+ mix={[
329
+ css({
330
+ display: 'flex',
331
+ alignItems: 'center',
332
+ gap: '6px',
333
+ padding: '8px 12px',
334
+ borderRadius: '6px',
335
+ color: '#fff',
336
+ cursor: 'pointer',
337
+ transition: 'background-color 150ms ease',
338
+ '&:hover': {
339
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
340
+ },
341
+ '&:has(input:checked)': {
342
+ backgroundColor: 'rgba(14, 165, 233, 0.3)',
343
+ },
344
+ }),
345
+ ]}
346
+ >
347
+ <input
348
+ type="radio"
349
+ name="spring-preset"
350
+ value={preset}
351
+ checked={selectedPreset === preset}
352
+ mix={[
353
+ css({ accentColor: '#0ea5e9' }),
354
+ on('change', () => {
355
+ selectedPreset = preset
356
+ springValue = spring(selectedPreset)
357
+ handle.update()
358
+ }),
359
+ ]}
360
+ />
361
+ {preset}
362
+ </label>
363
+ ))}
364
+ </div>
365
+
366
+ {/* Instructions */}
367
+ <div
368
+ mix={[
369
+ css({
370
+ position: 'absolute',
371
+ bottom: '20px',
372
+ left: '50%',
373
+ transform: 'translateX(-50%)',
374
+ color: '#ffffff80',
375
+ fontFamily: 'system-ui, sans-serif',
376
+ fontSize: '14px',
377
+ textAlign: 'center',
378
+ }),
379
+ ]}
380
+ >
381
+ Click to move target • Drag the blue circle and release to see velocity-based spring
382
+ </div>
383
+ </div>
384
+ )
385
+ }
@@ -0,0 +1,24 @@
1
+ import { css } from '@remix-run/ui'
2
+ import { animateEntrance } from '@remix-run/ui/animation'
3
+
4
+ export function TransitionOptions() {
5
+ return () => (
6
+ <div
7
+ mix={[
8
+ css({
9
+ width: 100,
10
+ height: 100,
11
+ borderRadius: '50%',
12
+ backgroundColor: '#9911ff',
13
+ }),
14
+ animateEntrance({
15
+ opacity: 0,
16
+ transform: 'scale(0.5)',
17
+ duration: 800,
18
+ delay: 500,
19
+ easing: 'cubic-bezier(0, 0.71, 0.2, 1.01)',
20
+ }),
21
+ ]}
22
+ />
23
+ )
24
+ }
@@ -38,6 +38,17 @@ export interface SpringIterator extends IterableIterator<number> {
38
38
  toString(): string
39
39
  }
40
40
 
41
+ export interface SpringFunction {
42
+ (preset: SpringPreset, overrides?: Omit<SpringOptions, 'bounce'>): SpringIterator
43
+ (options?: SpringOptions): SpringIterator
44
+ transition(
45
+ property: string | string[],
46
+ presetOrOptions?: SpringPreset | SpringOptions,
47
+ overrides?: Omit<SpringOptions, 'bounce'>,
48
+ ): string
49
+ presets: Record<SpringPreset, { duration: number; bounce: number }>
50
+ }
51
+
41
52
  const presets: Record<SpringPreset, { duration: number; bounce: number }> = {
42
53
  smooth: { duration: 400, bounce: -0.3 },
43
54
  snappy: { duration: 200, bounce: 0 },
@@ -74,7 +85,7 @@ const frameMs = 1000 / 60 // ~16.67ms per frame
74
85
  * @param overrides Optional preset overrides.
75
86
  * @returns A spring iterator.
76
87
  */
77
- export function spring(
88
+ function createSpring(
78
89
  preset: SpringPreset,
79
90
  overrides?: Omit<SpringOptions, 'bounce'>,
80
91
  ): SpringIterator
@@ -84,8 +95,8 @@ export function spring(
84
95
  * @param options Spring parameters.
85
96
  * @returns A spring iterator.
86
97
  */
87
- export function spring(options?: SpringOptions): SpringIterator
88
- export function spring(
98
+ function createSpring(options?: SpringOptions): SpringIterator
99
+ function createSpring(
89
100
  presetOrOptions?: SpringPreset | SpringOptions,
90
101
  overrides?: Omit<SpringOptions, 'bounce'>,
91
102
  ): SpringIterator {
@@ -119,23 +130,46 @@ export function spring(
119
130
  return iter as unknown as SpringIterator
120
131
  }
121
132
 
122
- // Transition helper for CSS transition property
123
- spring.transition = function transition(
133
+ function transition(
124
134
  property: string | string[],
125
135
  presetOrOptions?: SpringPreset | SpringOptions,
126
136
  overrides?: Omit<SpringOptions, 'bounce'>,
127
137
  ): string {
128
138
  let s =
129
139
  typeof presetOrOptions === 'string'
130
- ? spring(presetOrOptions, overrides)
131
- : spring(presetOrOptions)
140
+ ? createSpring(presetOrOptions, overrides)
141
+ : createSpring(presetOrOptions)
132
142
 
133
143
  let properties = Array.isArray(property) ? property : [property]
134
144
  return properties.map((p) => `${p} ${s}`).join(', ')
135
145
  }
136
146
 
137
- // Access preset defaults
138
- spring.presets = presets
147
+ /**
148
+ * Create a spring-physics animation iterator for CSS transitions and WAAPI.
149
+ *
150
+ * The returned iterator can be:
151
+ * - Iterated to get position values (0→1)
152
+ * - Spread into WAAPI options: `element.animate(keyframes, { ...spring() })`
153
+ * - Stringified for CSS: `` `transform ${spring()}` ``
154
+ *
155
+ * @example
156
+ * // Named preset
157
+ * let s = spring('bouncy')
158
+ * element.style.transition = `transform ${s}`
159
+ *
160
+ * @example
161
+ * // Custom parameters
162
+ * let s = spring({ duration: 500, bounce: 0.2 })
163
+ * element.animate(keyframes, { ...s })
164
+ *
165
+ * @example
166
+ * // Transition helper for multiple properties
167
+ * element.style.transition = spring.transition(['transform', 'opacity'], 'snappy')
168
+ */
169
+ export const spring: SpringFunction = Object.assign(createSpring, {
170
+ transition,
171
+ presets,
172
+ })
139
173
 
140
174
  function resolveOptions(
141
175
  presetOrOptions?: SpringPreset | SpringOptions,