@remix-run/ui 0.0.0 → 0.1.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 (273) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -2
  3. package/dist/animation/animate-layout-mixin.d.ts +12 -0
  4. package/dist/animation/animate-layout-mixin.js +214 -0
  5. package/dist/animation/animate-layout-mixin.js.map +1 -0
  6. package/dist/animation/animate-mixins.d.ts +29 -0
  7. package/dist/animation/animate-mixins.js +235 -0
  8. package/dist/animation/animate-mixins.js.map +1 -0
  9. package/dist/animation/index.d.ts +6 -0
  10. package/dist/animation/index.js +5 -0
  11. package/dist/animation/index.js.map +1 -0
  12. package/dist/animation/spring.d.ts +75 -0
  13. package/dist/animation/spring.js +183 -0
  14. package/dist/animation/spring.js.map +1 -0
  15. package/dist/animation/tween.d.ts +70 -0
  16. package/dist/animation/tween.js +93 -0
  17. package/dist/animation/tween.js.map +1 -0
  18. package/dist/components/accordion/accordion.d.ts +92 -0
  19. package/dist/components/accordion/accordion.js +337 -0
  20. package/dist/components/accordion/accordion.js.map +1 -0
  21. package/dist/components/anchor/anchor.d.ts +13 -0
  22. package/dist/components/anchor/anchor.js +558 -0
  23. package/dist/components/anchor/anchor.js.map +1 -0
  24. package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
  25. package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
  26. package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
  27. package/dist/components/button/button.d.ts +23 -0
  28. package/dist/components/button/button.js +147 -0
  29. package/dist/components/button/button.js.map +1 -0
  30. package/dist/components/combobox/combobox.d.ts +101 -0
  31. package/dist/components/combobox/combobox.js +708 -0
  32. package/dist/components/combobox/combobox.js.map +1 -0
  33. package/dist/components/glyph/glyph.d.ts +14 -0
  34. package/dist/components/glyph/glyph.js +65 -0
  35. package/dist/components/glyph/glyph.js.map +1 -0
  36. package/dist/components/listbox/listbox.d.ts +67 -0
  37. package/dist/components/listbox/listbox.js +340 -0
  38. package/dist/components/listbox/listbox.js.map +1 -0
  39. package/dist/components/menu/hover-aim.d.ts +5 -0
  40. package/dist/components/menu/hover-aim.js +308 -0
  41. package/dist/components/menu/hover-aim.js.map +1 -0
  42. package/dist/components/menu/menu.d.ts +164 -0
  43. package/dist/components/menu/menu.js +1106 -0
  44. package/dist/components/menu/menu.js.map +1 -0
  45. package/dist/components/popover/popover.d.ts +35 -0
  46. package/dist/components/popover/popover.js +138 -0
  47. package/dist/components/popover/popover.js.map +1 -0
  48. package/dist/components/select/select.d.ts +67 -0
  49. package/dist/components/select/select.js +397 -0
  50. package/dist/components/select/select.js.map +1 -0
  51. package/dist/components/separator/separator.d.ts +2 -0
  52. package/dist/components/separator/separator.js +15 -0
  53. package/dist/components/separator/separator.js.map +1 -0
  54. package/dist/components/tabs/tabs.d.ts +78 -0
  55. package/dist/components/tabs/tabs.js +341 -0
  56. package/dist/components/tabs/tabs.js.map +1 -0
  57. package/dist/index.d.ts +28 -0
  58. package/dist/index.js +21 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/interactions/keydown/keydown.d.ts +1 -0
  61. package/dist/interactions/keydown/keydown.js +8 -0
  62. package/dist/interactions/keydown/keydown.js.map +1 -0
  63. package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
  64. package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
  65. package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
  66. package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
  67. package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
  68. package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
  69. package/dist/jsx-runtime.d.ts +2 -0
  70. package/dist/jsx-runtime.js +3 -0
  71. package/dist/jsx-runtime.js.map +1 -0
  72. package/dist/runtime/client-entries.d.ts +91 -0
  73. package/dist/runtime/client-entries.js +40 -0
  74. package/dist/runtime/client-entries.js.map +1 -0
  75. package/dist/runtime/component.d.ts +223 -0
  76. package/dist/runtime/component.js +152 -0
  77. package/dist/runtime/component.js.map +1 -0
  78. package/dist/runtime/create-element.d.ts +10 -0
  79. package/dist/runtime/create-element.js +29 -0
  80. package/dist/runtime/create-element.js.map +1 -0
  81. package/dist/runtime/diff-dom.d.ts +2 -0
  82. package/dist/runtime/diff-dom.js +364 -0
  83. package/dist/runtime/diff-dom.js.map +1 -0
  84. package/dist/runtime/diff-props.d.ts +9 -0
  85. package/dist/runtime/diff-props.js +195 -0
  86. package/dist/runtime/diff-props.js.map +1 -0
  87. package/dist/runtime/document-state.d.ts +11 -0
  88. package/dist/runtime/document-state.js +106 -0
  89. package/dist/runtime/document-state.js.map +1 -0
  90. package/dist/runtime/dom.d.ts +3189 -0
  91. package/dist/runtime/dom.js +2 -0
  92. package/dist/runtime/dom.js.map +1 -0
  93. package/dist/runtime/error-event.d.ts +20 -0
  94. package/dist/runtime/error-event.js +19 -0
  95. package/dist/runtime/error-event.js.map +1 -0
  96. package/dist/runtime/event-listeners.d.ts +50 -0
  97. package/dist/runtime/event-listeners.js +31 -0
  98. package/dist/runtime/event-listeners.js.map +1 -0
  99. package/dist/runtime/frame.d.ts +119 -0
  100. package/dist/runtime/frame.js +915 -0
  101. package/dist/runtime/frame.js.map +1 -0
  102. package/dist/runtime/invariant.d.ts +23 -0
  103. package/dist/runtime/invariant.js +33 -0
  104. package/dist/runtime/invariant.js.map +1 -0
  105. package/dist/runtime/jsx.d.ts +320 -0
  106. package/dist/runtime/jsx.js +31 -0
  107. package/dist/runtime/jsx.js.map +1 -0
  108. package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
  109. package/dist/runtime/mixins/attrs-mixin.js +23 -0
  110. package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
  111. package/dist/runtime/mixins/link-mixin.d.ts +17 -0
  112. package/dist/runtime/mixins/link-mixin.js +101 -0
  113. package/dist/runtime/mixins/link-mixin.js.map +1 -0
  114. package/dist/runtime/mixins/mixin.d.ts +145 -0
  115. package/dist/runtime/mixins/mixin.js +570 -0
  116. package/dist/runtime/mixins/mixin.js.map +1 -0
  117. package/dist/runtime/mixins/on-mixin.d.ts +16 -0
  118. package/dist/runtime/mixins/on-mixin.js +49 -0
  119. package/dist/runtime/mixins/on-mixin.js.map +1 -0
  120. package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
  121. package/dist/runtime/mixins/ref-mixin.js +21 -0
  122. package/dist/runtime/mixins/ref-mixin.js.map +1 -0
  123. package/dist/runtime/navigation.d.ts +22 -0
  124. package/dist/runtime/navigation.js +102 -0
  125. package/dist/runtime/navigation.js.map +1 -0
  126. package/dist/runtime/reconcile.d.ts +11 -0
  127. package/dist/runtime/reconcile.js +1240 -0
  128. package/dist/runtime/reconcile.js.map +1 -0
  129. package/dist/runtime/render.d.ts +54 -0
  130. package/dist/runtime/render.js +50 -0
  131. package/dist/runtime/render.js.map +1 -0
  132. package/dist/runtime/run.d.ts +45 -0
  133. package/dist/runtime/run.js +68 -0
  134. package/dist/runtime/run.js.map +1 -0
  135. package/dist/runtime/scheduler.d.ts +30 -0
  136. package/dist/runtime/scheduler.js +185 -0
  137. package/dist/runtime/scheduler.js.map +1 -0
  138. package/dist/runtime/svg-attributes.d.ts +5 -0
  139. package/dist/runtime/svg-attributes.js +124 -0
  140. package/dist/runtime/svg-attributes.js.map +1 -0
  141. package/dist/runtime/to-vnode.d.ts +3 -0
  142. package/dist/runtime/to-vnode.js +43 -0
  143. package/dist/runtime/to-vnode.js.map +1 -0
  144. package/dist/runtime/typed-event-target.d.ts +50 -0
  145. package/dist/runtime/typed-event-target.js +6 -0
  146. package/dist/runtime/typed-event-target.js.map +1 -0
  147. package/dist/runtime/vdom.d.ts +55 -0
  148. package/dist/runtime/vdom.js +198 -0
  149. package/dist/runtime/vdom.js.map +1 -0
  150. package/dist/runtime/vnode.d.ts +79 -0
  151. package/dist/runtime/vnode.js +38 -0
  152. package/dist/runtime/vnode.js.map +1 -0
  153. package/dist/server/stream.d.ts +57 -0
  154. package/dist/server/stream.js +1007 -0
  155. package/dist/server/stream.js.map +1 -0
  156. package/dist/style/css-mixin.d.ts +8 -0
  157. package/dist/style/css-mixin.js +54 -0
  158. package/dist/style/css-mixin.js.map +1 -0
  159. package/dist/style/index.d.ts +5 -0
  160. package/dist/style/index.js +4 -0
  161. package/dist/style/index.js.map +1 -0
  162. package/dist/style/style.d.ts +24 -0
  163. package/dist/style/style.js +269 -0
  164. package/dist/style/style.js.map +1 -0
  165. package/dist/style/stylesheet.d.ts +11 -0
  166. package/dist/style/stylesheet.js +174 -0
  167. package/dist/style/stylesheet.js.map +1 -0
  168. package/dist/test.d.ts +2 -0
  169. package/dist/test.js +2 -0
  170. package/dist/test.js.map +1 -0
  171. package/dist/theme/contract.d.ts +259 -0
  172. package/dist/theme/contract.js +131 -0
  173. package/dist/theme/contract.js.map +1 -0
  174. package/dist/theme/glyph-contract.d.ts +13 -0
  175. package/dist/theme/glyph-contract.js +34 -0
  176. package/dist/theme/glyph-contract.js.map +1 -0
  177. package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
  178. package/dist/theme/presets/rmx-01/glyphs.js +252 -0
  179. package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
  180. package/dist/theme/presets/rmx-01/index.d.ts +2 -0
  181. package/dist/theme/presets/rmx-01/index.js +119 -0
  182. package/dist/theme/presets/rmx-01/index.js.map +1 -0
  183. package/dist/theme/runtime.d.ts +2 -0
  184. package/dist/theme/runtime.js +81 -0
  185. package/dist/theme/runtime.js.map +1 -0
  186. package/dist/theme/theme.d.ts +6 -0
  187. package/dist/theme/theme.js +5 -0
  188. package/dist/theme/theme.js.map +1 -0
  189. package/dist/utils/flash-attribute.d.ts +1 -0
  190. package/dist/utils/flash-attribute.js +11 -0
  191. package/dist/utils/flash-attribute.js.map +1 -0
  192. package/dist/utils/scroll-lock.d.ts +3 -0
  193. package/dist/utils/scroll-lock.js +69 -0
  194. package/dist/utils/scroll-lock.js.map +1 -0
  195. package/dist/utils/wait-for-css-transition.d.ts +1 -0
  196. package/dist/utils/wait-for-css-transition.js +58 -0
  197. package/dist/utils/wait-for-css-transition.js.map +1 -0
  198. package/dist/utils/wait.d.ts +1 -0
  199. package/dist/utils/wait.js +6 -0
  200. package/dist/utils/wait.js.map +1 -0
  201. package/package.json +125 -5
  202. package/src/animation/animate-layout-mixin.ts +266 -0
  203. package/src/animation/animate-mixins.ts +292 -0
  204. package/src/animation/index.ts +6 -0
  205. package/src/animation/spring.ts +299 -0
  206. package/src/animation/tween.ts +134 -0
  207. package/src/components/accordion/accordion.tsx +565 -0
  208. package/src/components/anchor/anchor.ts +904 -0
  209. package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
  210. package/src/components/button/README.md +44 -0
  211. package/src/components/button/button.tsx +188 -0
  212. package/src/components/combobox/README.md +145 -0
  213. package/src/components/combobox/combobox.tsx +1014 -0
  214. package/src/components/glyph/glyph.tsx +110 -0
  215. package/src/components/listbox/listbox.ts +447 -0
  216. package/src/components/menu/hover-aim.ts +428 -0
  217. package/src/components/menu/menu.tsx +1547 -0
  218. package/src/components/popover/README.md +122 -0
  219. package/src/components/popover/popover.ts +201 -0
  220. package/src/components/select/select.tsx +585 -0
  221. package/src/components/separator/separator.ts +19 -0
  222. package/src/components/tabs/README.md +105 -0
  223. package/src/components/tabs/tabs.tsx +499 -0
  224. package/src/index.ts +72 -0
  225. package/src/interactions/keydown/keydown.ts +14 -0
  226. package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
  227. package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
  228. package/src/jsx-runtime.ts +2 -0
  229. package/src/runtime/client-entries.ts +137 -0
  230. package/src/runtime/component.ts +402 -0
  231. package/src/runtime/create-element.ts +38 -0
  232. package/src/runtime/diff-dom.ts +404 -0
  233. package/src/runtime/diff-props.ts +209 -0
  234. package/src/runtime/document-state.ts +130 -0
  235. package/src/runtime/dom.ts +3971 -0
  236. package/src/runtime/error-event.ts +26 -0
  237. package/src/runtime/event-listeners.ts +171 -0
  238. package/src/runtime/frame.ts +1211 -0
  239. package/src/runtime/invariant.ts +31 -0
  240. package/src/runtime/jsx.ts +398 -0
  241. package/src/runtime/mixins/attrs-mixin.ts +35 -0
  242. package/src/runtime/mixins/link-mixin.ts +131 -0
  243. package/src/runtime/mixins/mixin.ts +908 -0
  244. package/src/runtime/mixins/on-mixin.ts +89 -0
  245. package/src/runtime/mixins/ref-mixin.ts +32 -0
  246. package/src/runtime/navigation.ts +136 -0
  247. package/src/runtime/reconcile.ts +1722 -0
  248. package/src/runtime/render.ts +101 -0
  249. package/src/runtime/run.ts +100 -0
  250. package/src/runtime/scheduler.ts +262 -0
  251. package/src/runtime/svg-attributes.ts +133 -0
  252. package/src/runtime/to-vnode.ts +50 -0
  253. package/src/runtime/typed-event-target.ts +67 -0
  254. package/src/runtime/vdom.ts +295 -0
  255. package/src/runtime/vnode.ts +137 -0
  256. package/src/server/stream.ts +1342 -0
  257. package/src/style/css-mixin.ts +82 -0
  258. package/src/style/index.ts +10 -0
  259. package/src/style/style.ts +312 -0
  260. package/src/style/stylesheet.ts +209 -0
  261. package/src/test/setup.ts +7 -0
  262. package/src/test/utils.ts +45 -0
  263. package/src/test.ts +2 -0
  264. package/src/theme/contract.ts +183 -0
  265. package/src/theme/glyph-contract.ts +57 -0
  266. package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
  267. package/src/theme/presets/rmx-01/index.ts +121 -0
  268. package/src/theme/runtime.ts +116 -0
  269. package/src/theme/theme.ts +15 -0
  270. package/src/utils/flash-attribute.ts +11 -0
  271. package/src/utils/scroll-lock.ts +97 -0
  272. package/src/utils/wait-for-css-transition.ts +66 -0
  273. package/src/utils/wait.ts +5 -0
@@ -0,0 +1,292 @@
1
+ import { createMixin } from '../runtime/mixins/mixin.ts'
2
+ import type { ElementProps } from '../runtime/jsx.ts'
3
+ import type { MixinDescriptor } from '../runtime/mixins/mixin.ts'
4
+ import { invariant } from '../runtime/invariant.ts'
5
+
6
+ type AnimateTiming = {
7
+ duration: number
8
+ easing?: string
9
+ delay?: number
10
+ composite?: CompositeOperation
11
+ initial?: boolean
12
+ }
13
+
14
+ type AnimateStyleProps = {
15
+ [property: string]: unknown
16
+ }
17
+
18
+ export type AnimateMixinConfig = AnimateTiming & AnimateStyleProps
19
+
20
+ type AnimationConfig = true | false | null | undefined | AnimateMixinConfig
21
+
22
+ const DEFAULT_ENTER: AnimateMixinConfig = {
23
+ opacity: 0,
24
+ duration: 150,
25
+ easing: 'ease-out',
26
+ }
27
+
28
+ const DEFAULT_EXIT: AnimateMixinConfig = {
29
+ opacity: 0,
30
+ duration: 150,
31
+ easing: 'ease-in',
32
+ }
33
+
34
+ type AnimationState = {
35
+ animation: Animation
36
+ properties: string[]
37
+ }
38
+
39
+ const animatingNodes = new WeakMap<Element, AnimationState>()
40
+ const initialEntranceSeenByParent = new WeakMap<ParentNode, Set<string>>()
41
+
42
+ function extractStyleProps(config: AnimateMixinConfig): Keyframe {
43
+ let result: Keyframe = {}
44
+ for (let key in config) {
45
+ if (
46
+ key === 'duration' ||
47
+ key === 'easing' ||
48
+ key === 'delay' ||
49
+ key === 'composite' ||
50
+ key === 'initial'
51
+ ) {
52
+ continue
53
+ }
54
+ let value = config[key]
55
+ if (value === undefined) continue
56
+ if (typeof value !== 'string' && typeof value !== 'number') continue
57
+ result[key as keyof Keyframe] = value
58
+ }
59
+ return result
60
+ }
61
+
62
+ function buildEnterKeyframes(config: AnimateMixinConfig): Keyframe[] {
63
+ let keyframe = extractStyleProps(config)
64
+ return [keyframe, {}]
65
+ }
66
+
67
+ function buildExitKeyframes(config: AnimateMixinConfig): Keyframe[] {
68
+ let keyframe = extractStyleProps(config)
69
+ return [{}, keyframe]
70
+ }
71
+
72
+ function resolveEnterConfig(config: AnimationConfig): AnimateMixinConfig | null {
73
+ if (!config) return null
74
+ if (config === true) return DEFAULT_ENTER
75
+ return config
76
+ }
77
+
78
+ function resolveExitConfig(config: AnimationConfig): AnimateMixinConfig | null {
79
+ if (!config) return null
80
+ if (config === true) return DEFAULT_EXIT
81
+ return config
82
+ }
83
+
84
+ function createAnimationOptions(
85
+ config: AnimateMixinConfig,
86
+ fill: FillMode,
87
+ ): KeyframeAnimationOptions {
88
+ return {
89
+ duration: config.duration,
90
+ delay: config.delay,
91
+ easing: config.easing,
92
+ composite: config.composite,
93
+ fill,
94
+ }
95
+ }
96
+
97
+ function collectAnimatedProperties(keyframes: Keyframe[]): string[] {
98
+ let properties = new Set<string>()
99
+ for (let keyframe of keyframes) {
100
+ for (let key in keyframe) {
101
+ if (key === 'offset' || key === 'easing' || key === 'composite') continue
102
+ properties.add(key)
103
+ }
104
+ }
105
+ return [...properties]
106
+ }
107
+
108
+ function toCssPropertyName(property: string): string {
109
+ return property.includes('-')
110
+ ? property
111
+ : property.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)
112
+ }
113
+
114
+ function readInlineStyle(style: CSSStyleDeclaration, property: string): string {
115
+ return style.getPropertyValue(toCssPropertyName(property))
116
+ }
117
+
118
+ function writeInlineStyle(style: CSSStyleDeclaration, property: string, value: string): void {
119
+ let cssProperty = toCssPropertyName(property)
120
+ if (value === '') {
121
+ style.removeProperty(cssProperty)
122
+ return
123
+ }
124
+ style.setProperty(cssProperty, value)
125
+ }
126
+
127
+ function trackAnimation(node: Element, animation: Animation, keyframes: Keyframe[]) {
128
+ let properties = collectAnimatedProperties(keyframes)
129
+ animatingNodes.set(node, { animation, properties })
130
+ animation.finished
131
+ .catch(() => {})
132
+ .finally(() => {
133
+ let current = animatingNodes.get(node)
134
+ if (current?.animation !== animation) return
135
+ animatingNodes.delete(node)
136
+ })
137
+ }
138
+
139
+ function waitForAnimationOrAbort(animation: Animation, signal: AbortSignal): Promise<void> {
140
+ if (signal.aborted) return Promise.resolve()
141
+ return new Promise((resolve) => {
142
+ let settled = false
143
+ let settle = () => {
144
+ if (settled) return
145
+ settled = true
146
+ signal.removeEventListener('abort', settle)
147
+ resolve()
148
+ }
149
+ signal.addEventListener('abort', settle, { once: true })
150
+ void animation.finished.catch(() => {}).finally(settle)
151
+ })
152
+ }
153
+
154
+ function shouldSkipInitialEntrance(
155
+ event: { key?: string; parent: ParentNode },
156
+ config: AnimateMixinConfig,
157
+ ): boolean {
158
+ if (config.initial !== false) return false
159
+ if (event.key == null) return false
160
+ let seenForParent = initialEntranceSeenByParent.get(event.parent)
161
+ if (!seenForParent) {
162
+ seenForParent = new Set<string>()
163
+ initialEntranceSeenByParent.set(event.parent, seenForParent)
164
+ }
165
+ if (seenForParent.has(event.key)) return false
166
+ seenForParent.add(event.key)
167
+ return true
168
+ }
169
+
170
+ const animateEntranceMixin = createMixin<Element, [config: AnimationConfig], ElementProps>(
171
+ (handle) => {
172
+ let currentConfig: AnimationConfig = true
173
+
174
+ handle.addEventListener('insert', (event) => {
175
+ let node = event.node
176
+ let current = animatingNodes.get(node)
177
+ if (current && current.animation.playState === 'running') {
178
+ return
179
+ }
180
+
181
+ let config = resolveEnterConfig(currentConfig)
182
+ if (!config) return
183
+ if (shouldSkipInitialEntrance(event, config)) return
184
+ let keyframes = buildEnterKeyframes(config)
185
+ let options = createAnimationOptions(config, 'backwards')
186
+ let animation = (node as HTMLElement).animate(keyframes, options)
187
+ trackAnimation(node, animation, keyframes)
188
+ })
189
+
190
+ return (config) => {
191
+ currentConfig = config
192
+ return handle.element
193
+ }
194
+ },
195
+ )
196
+
197
+ const animateExitMixin = createMixin<Element, [config: AnimationConfig], ElementProps>((handle) => {
198
+ let currentConfig: AnimationConfig = true
199
+ let node: Element | null = null
200
+
201
+ handle.addEventListener('insert', (event) => {
202
+ node = event.node
203
+ })
204
+
205
+ handle.addEventListener('reclaimed', (event) => {
206
+ node = event.node
207
+ let current = animatingNodes.get(event.node)
208
+ if (current && current.animation.playState === 'running') {
209
+ // WAAPI can throw InvalidStateError here if the target is transiently non-rendered
210
+ // during reclaim; we still have computed-style fallback below for retargeting.
211
+ try {
212
+ current.animation.commitStyles()
213
+ } catch {}
214
+ current.animation.cancel()
215
+
216
+ let style = (event.node as HTMLElement).style
217
+ let computed = getComputedStyle(event.node as Element)
218
+ let from: Keyframe = {}
219
+ for (let property of current.properties) {
220
+ let cssProperty = toCssPropertyName(property)
221
+ let value = readInlineStyle(style, property) || computed.getPropertyValue(cssProperty)
222
+ if (value !== '') {
223
+ from[property as keyof Keyframe] = value
224
+ }
225
+ writeInlineStyle(style, property, '')
226
+ }
227
+
228
+ let enterConfig = resolveEnterConfig(currentConfig) ?? DEFAULT_ENTER
229
+ let keyframes: Keyframe[] = [from, {}]
230
+ let options = createAnimationOptions(enterConfig, 'none')
231
+ let animation = (event.node as HTMLElement).animate(keyframes, options)
232
+ trackAnimation(event.node, animation, keyframes)
233
+ }
234
+ })
235
+
236
+ handle.addEventListener('beforeRemove', (event) => {
237
+ let config = resolveExitConfig(currentConfig)
238
+ if (!config) return
239
+ event.persistNode(async (signal) => {
240
+ invariant(node)
241
+ let current = animatingNodes.get(node)
242
+ if (current && current.animation.playState === 'running') {
243
+ current.animation.reverse()
244
+ await waitForAnimationOrAbort(current.animation, signal)
245
+ return
246
+ }
247
+
248
+ let keyframes = buildExitKeyframes(config)
249
+ let options = createAnimationOptions(config, 'forwards')
250
+ let animation = (node as HTMLElement).animate(keyframes, options)
251
+ trackAnimation(node, animation, keyframes)
252
+ await waitForAnimationOrAbort(animation, signal)
253
+ })
254
+ })
255
+
256
+ return (config) => {
257
+ currentConfig = config
258
+ return handle.element
259
+ }
260
+ })
261
+
262
+ /**
263
+ * Animates an element when it is inserted into the DOM.
264
+ *
265
+ * @param config Entrance animation configuration.
266
+ * @returns A mixin descriptor for the target element.
267
+ */
268
+ export function animateEntrance<target extends EventTarget = Element>(
269
+ config: AnimationConfig = true,
270
+ ): MixinDescriptor<target, [AnimationConfig], ElementProps> {
271
+ return animateEntranceMixin(config) as unknown as MixinDescriptor<
272
+ target,
273
+ [AnimationConfig],
274
+ ElementProps
275
+ >
276
+ }
277
+
278
+ /**
279
+ * Animates an element when it is removed from the DOM.
280
+ *
281
+ * @param config Exit animation configuration.
282
+ * @returns A mixin descriptor for the target element.
283
+ */
284
+ export function animateExit<target extends EventTarget = Element>(
285
+ config: AnimationConfig = true,
286
+ ): MixinDescriptor<target, [AnimationConfig], ElementProps> {
287
+ return animateExitMixin(config) as unknown as MixinDescriptor<
288
+ target,
289
+ [AnimationConfig],
290
+ ElementProps
291
+ >
292
+ }
@@ -0,0 +1,6 @@
1
+ export { animateEntrance, animateExit } from './animate-mixins.ts'
2
+ export { animateLayout } from './animate-layout-mixin.ts'
3
+ export { spring } from './spring.ts'
4
+ export type { SpringIterator, SpringPreset, SpringOptions } from './spring.ts'
5
+ export { tween, easings } from './tween.ts'
6
+ export type { TweenOptions, BezierCurve } from './tween.ts'
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Spring physics based on SwiftUI's spring math.
3
+ *
4
+ * Returns a decorated iterator that can be:
5
+ * - Iterated to get position values (0→1)
6
+ * - Spread for WAAPI: { ...spring() }
7
+ * - Stringified for CSS: `transform ${spring()}`
8
+ *
9
+ * Spring Parameter Conversion (SwiftUI formulas, mass = 1):
10
+ * stiffness = (2π ÷ duration)²
11
+ * damping = 1 - 4π × bounce ÷ duration (bounce ≥ 0)
12
+ * damping = 4π ÷ (duration + 4π × bounce) (bounce < 0)
13
+ */
14
+
15
+ export type SpringPreset = 'smooth' | 'snappy' | 'bouncy'
16
+
17
+ /**
18
+ * Options for generating a spring easing iterator.
19
+ */
20
+ export interface SpringOptions {
21
+ /** Perceptual duration in milliseconds used to derive spring stiffness. */
22
+ duration?: number // perceptual duration in ms (default: 300) - affects stiffness
23
+ /** Spring bounce amount from overdamped (`< 0`) to bouncy (`> 0`). */
24
+ bounce?: number // -1 to ~0.95: negative = overdamped, 0 = critical, positive = bouncy
25
+ /** Initial velocity in units per second. */
26
+ velocity?: number // initial velocity in units per second
27
+ }
28
+
29
+ /**
30
+ * Iterator returned by {@link spring}, decorated for CSS and WAAPI use.
31
+ */
32
+ export interface SpringIterator extends IterableIterator<number> {
33
+ /** Time when spring settles to rest (milliseconds) */
34
+ duration: number
35
+ /** CSS linear() easing function */
36
+ easing: string
37
+ /** Returns "duration ms linear(...)" for CSS transitions */
38
+ toString(): string
39
+ }
40
+
41
+ const presets: Record<SpringPreset, { duration: number; bounce: number }> = {
42
+ smooth: { duration: 400, bounce: -0.3 },
43
+ snappy: { duration: 200, bounce: 0 },
44
+ bouncy: { duration: 400, bounce: 0.3 },
45
+ }
46
+
47
+ // Rest thresholds for determining when spring has settled
48
+ const restSpeed = 0.01
49
+ const restDelta = 0.005
50
+ const maxSettlingTime = 20_000
51
+ const frameMs = 1000 / 60 // ~16.67ms per frame
52
+
53
+ /**
54
+ * Create a spring iterator for animations.
55
+ *
56
+ * @example
57
+ * let s = spring('bouncy')
58
+ *
59
+ * // As CSS transition
60
+ * element.style.transition = `transform ${s}`
61
+ *
62
+ * // Spread for WAAPI
63
+ * element.animate(keyframes, { ...spring() })
64
+ *
65
+ * // Iterate for JS animation
66
+ * for (let position of spring()) {
67
+ * element.style.transform = `translateX(${position * 100}px)`
68
+ * }
69
+ */
70
+ /**
71
+ * Creates a spring iterator from a named preset.
72
+ *
73
+ * @param preset Preset spring profile to start from.
74
+ * @param overrides Optional preset overrides.
75
+ * @returns A spring iterator.
76
+ */
77
+ export function spring(
78
+ preset: SpringPreset,
79
+ overrides?: Omit<SpringOptions, 'bounce'>,
80
+ ): SpringIterator
81
+ /**
82
+ * Creates a spring iterator from explicit spring options.
83
+ *
84
+ * @param options Spring parameters.
85
+ * @returns A spring iterator.
86
+ */
87
+ export function spring(options?: SpringOptions): SpringIterator
88
+ export function spring(
89
+ presetOrOptions?: SpringPreset | SpringOptions,
90
+ overrides?: Omit<SpringOptions, 'bounce'>,
91
+ ): SpringIterator {
92
+ let options = resolveOptions(presetOrOptions, overrides)
93
+ let { position, settlingTime, easing } = computeSpring(options)
94
+
95
+ let duration = Math.round(settlingTime)
96
+
97
+ function* generator(): Generator<number, void, undefined> {
98
+ let t = 0
99
+ while (t < settlingTime) {
100
+ yield position(t)
101
+ t += frameMs
102
+ }
103
+ yield 1
104
+ }
105
+
106
+ let iter = generator()
107
+
108
+ // Decorate iterator with spring properties (enumerable for spread)
109
+ Object.defineProperties(iter, {
110
+ duration: { value: duration, enumerable: true },
111
+ easing: { value: easing, enumerable: true },
112
+ toString: {
113
+ value() {
114
+ return `${duration}ms ${easing}`
115
+ },
116
+ },
117
+ })
118
+
119
+ return iter as unknown as SpringIterator
120
+ }
121
+
122
+ // Transition helper for CSS transition property
123
+ spring.transition = function transition(
124
+ property: string | string[],
125
+ presetOrOptions?: SpringPreset | SpringOptions,
126
+ overrides?: Omit<SpringOptions, 'bounce'>,
127
+ ): string {
128
+ let s =
129
+ typeof presetOrOptions === 'string'
130
+ ? spring(presetOrOptions, overrides)
131
+ : spring(presetOrOptions)
132
+
133
+ let properties = Array.isArray(property) ? property : [property]
134
+ return properties.map((p) => `${p} ${s}`).join(', ')
135
+ }
136
+
137
+ // Access preset defaults
138
+ spring.presets = presets
139
+
140
+ function resolveOptions(
141
+ presetOrOptions?: SpringPreset | SpringOptions,
142
+ overrides?: Omit<SpringOptions, 'bounce'>,
143
+ ): SpringOptions {
144
+ if (typeof presetOrOptions === 'string') {
145
+ let preset = presets[presetOrOptions]
146
+ return {
147
+ duration: overrides?.duration ?? preset.duration,
148
+ bounce: preset.bounce,
149
+ velocity: overrides?.velocity,
150
+ }
151
+ }
152
+ if (presetOrOptions) {
153
+ return presetOrOptions
154
+ }
155
+ // Default to 'snappy' preset
156
+ return presets.snappy
157
+ }
158
+
159
+ interface ComputedSpring {
160
+ position: (t: number) => number
161
+ settlingTime: number
162
+ easing: string
163
+ }
164
+
165
+ // Core spring computation
166
+ function computeSpring(options: SpringOptions): ComputedSpring {
167
+ let { duration: durationMs = 300, bounce = 0, velocity = 0 } = options
168
+
169
+ // Convert duration to seconds for physics calculations
170
+ let durationSec = durationMs / 1000
171
+
172
+ // Natural frequency: ω₀ = √(stiffness) = 2π / duration
173
+ let omega0 = (2 * Math.PI) / durationSec
174
+
175
+ // Clamp bounce to valid range
176
+ bounce = Math.max(-1, Math.min(0.95, bounce))
177
+
178
+ // Damping ratio (ζ):
179
+ // bounce >= 0: ζ = 1 - bounce (linear, 0→1 maps to critical→underdamped)
180
+ // bounce < 0: ζ = 1 / (1 + bounce) (stronger overdamping as bounce→-1)
181
+ let zeta = bounce >= 0 ? 1 - bounce : 1 / (1 + bounce)
182
+
183
+ // Convert to per-millisecond for time calculations
184
+ let omega0Ms = omega0 / 1000
185
+ let velocityMs = -velocity / 1000 // negated for spring equation convention
186
+
187
+ // Position function based on damping regime
188
+ let position: (t: number) => number
189
+
190
+ if (zeta < 1) {
191
+ // Underdamped (bouncy) - oscillates around target
192
+ let omegaD = omega0Ms * Math.sqrt(1 - zeta * zeta)
193
+ position = (t: number) => {
194
+ let envelope = Math.exp(-zeta * omega0Ms * t)
195
+ return (
196
+ 1 -
197
+ envelope *
198
+ (((velocityMs + zeta * omega0Ms) / omegaD) * Math.sin(omegaD * t) + Math.cos(omegaD * t))
199
+ )
200
+ }
201
+ } else if (zeta > 1) {
202
+ // Overdamped (smooth) - no oscillation, two decay rates
203
+ let sqrtTerm = Math.sqrt(zeta * zeta - 1)
204
+ let s1 = omega0Ms * (-zeta + sqrtTerm) // slower decay
205
+ let s2 = omega0Ms * (-zeta - sqrtTerm) // faster decay
206
+ let A = (s2 + velocityMs) / (s2 - s1)
207
+ let B = 1 - A
208
+ position = (t: number) => 1 - A * Math.exp(s1 * t) - B * Math.exp(s2 * t)
209
+ } else {
210
+ // Critically damped - fastest approach without oscillation
211
+ position = (t: number) => 1 - Math.exp(-omega0Ms * t) * (1 + (velocityMs + omega0Ms) * t)
212
+ }
213
+
214
+ // Velocity via numerical differentiation (units per second)
215
+ let velocitySampleMs = 0.5
216
+ function velocityAt(t: number): number {
217
+ if (t < velocitySampleMs) {
218
+ return ((position(velocitySampleMs) - position(0)) / velocitySampleMs) * 1000
219
+ }
220
+ return ((position(t) - position(t - velocitySampleMs)) / velocitySampleMs) * 1000
221
+ }
222
+
223
+ // Find settling time
224
+ let settlingTime = maxSettlingTime
225
+ let step = 50
226
+
227
+ for (let t = 0; t < maxSettlingTime; t += step) {
228
+ let pos = position(t)
229
+ let vel = Math.abs(velocityAt(t))
230
+ let displacement = Math.abs(1 - pos)
231
+
232
+ if (vel <= restSpeed && displacement <= restDelta) {
233
+ settlingTime = t
234
+ break
235
+ }
236
+ }
237
+
238
+ // Generate CSS easing
239
+ let easing = generateEasing(position, settlingTime)
240
+
241
+ return { position, settlingTime, easing }
242
+ }
243
+
244
+ // Generate CSS linear() easing with adaptive sampling
245
+ function generateEasing(position: (t: number) => number, settlingTime: number): string {
246
+ let points = adaptiveSample(position, settlingTime)
247
+
248
+ return `linear(${points
249
+ .map((p, i) => {
250
+ let isLast = i === points.length - 1
251
+ let value = isLast ? 1 : Math.round(p.value * 10000) / 10000
252
+ if (i === 0 || isLast) {
253
+ return value === 1 ? '1' : value.toString()
254
+ }
255
+ let percent = Math.round((p.t / settlingTime) * 1000) / 10
256
+ return `${value} ${percent}%`
257
+ })
258
+ .join(', ')})`
259
+ }
260
+
261
+ // Adaptive sampling - more points where curvature is high, fewer where linear
262
+ function adaptiveSample(
263
+ resolve: (t: number) => number,
264
+ duration: number,
265
+ tolerance: number = 0.002,
266
+ minSegment: number = 8,
267
+ ): Array<{ t: number; value: number }> {
268
+ let points: Array<{ t: number; value: number }> = []
269
+
270
+ function addPoint(t: number, value: number) {
271
+ if (points.length === 0 || points[points.length - 1].t < t) {
272
+ points.push({ t, value })
273
+ }
274
+ }
275
+
276
+ function subdivide(t0: number, v0: number, t1: number, v1: number, depth: number = 0) {
277
+ if (depth > 12) {
278
+ addPoint(t0, v0)
279
+ return
280
+ }
281
+
282
+ let tMid = (t0 + t1) / 2
283
+ let vMid = resolve(tMid)
284
+ let vLinear = (v0 + v1) / 2
285
+ let error = Math.abs(vMid - vLinear)
286
+
287
+ if (error > tolerance && t1 - t0 > minSegment) {
288
+ subdivide(t0, v0, tMid, vMid, depth + 1)
289
+ subdivide(tMid, vMid, t1, v1, depth + 1)
290
+ } else {
291
+ addPoint(t0, v0)
292
+ }
293
+ }
294
+
295
+ subdivide(0, resolve(0), duration, resolve(duration))
296
+ addPoint(duration, resolve(duration))
297
+
298
+ return points
299
+ }