@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.
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/animation/animate-layout-mixin.d.ts +12 -0
- package/dist/animation/animate-layout-mixin.js +214 -0
- package/dist/animation/animate-layout-mixin.js.map +1 -0
- package/dist/animation/animate-mixins.d.ts +29 -0
- package/dist/animation/animate-mixins.js +235 -0
- package/dist/animation/animate-mixins.js.map +1 -0
- package/dist/animation/index.d.ts +6 -0
- package/dist/animation/index.js +5 -0
- package/dist/animation/index.js.map +1 -0
- package/dist/animation/spring.d.ts +75 -0
- package/dist/animation/spring.js +183 -0
- package/dist/animation/spring.js.map +1 -0
- package/dist/animation/tween.d.ts +70 -0
- package/dist/animation/tween.js +93 -0
- package/dist/animation/tween.js.map +1 -0
- package/dist/components/accordion/accordion.d.ts +92 -0
- package/dist/components/accordion/accordion.js +337 -0
- package/dist/components/accordion/accordion.js.map +1 -0
- package/dist/components/anchor/anchor.d.ts +13 -0
- package/dist/components/anchor/anchor.js +558 -0
- package/dist/components/anchor/anchor.js.map +1 -0
- package/dist/components/breadcrumbs/breadcrumbs.d.ts +11 -0
- package/dist/components/breadcrumbs/breadcrumbs.js +78 -0
- package/dist/components/breadcrumbs/breadcrumbs.js.map +1 -0
- package/dist/components/button/button.d.ts +23 -0
- package/dist/components/button/button.js +147 -0
- package/dist/components/button/button.js.map +1 -0
- package/dist/components/combobox/combobox.d.ts +101 -0
- package/dist/components/combobox/combobox.js +708 -0
- package/dist/components/combobox/combobox.js.map +1 -0
- package/dist/components/glyph/glyph.d.ts +14 -0
- package/dist/components/glyph/glyph.js +65 -0
- package/dist/components/glyph/glyph.js.map +1 -0
- package/dist/components/listbox/listbox.d.ts +67 -0
- package/dist/components/listbox/listbox.js +340 -0
- package/dist/components/listbox/listbox.js.map +1 -0
- package/dist/components/menu/hover-aim.d.ts +5 -0
- package/dist/components/menu/hover-aim.js +308 -0
- package/dist/components/menu/hover-aim.js.map +1 -0
- package/dist/components/menu/menu.d.ts +164 -0
- package/dist/components/menu/menu.js +1106 -0
- package/dist/components/menu/menu.js.map +1 -0
- package/dist/components/popover/popover.d.ts +35 -0
- package/dist/components/popover/popover.js +138 -0
- package/dist/components/popover/popover.js.map +1 -0
- package/dist/components/select/select.d.ts +67 -0
- package/dist/components/select/select.js +397 -0
- package/dist/components/select/select.js.map +1 -0
- package/dist/components/separator/separator.d.ts +2 -0
- package/dist/components/separator/separator.js +15 -0
- package/dist/components/separator/separator.js.map +1 -0
- package/dist/components/tabs/tabs.d.ts +78 -0
- package/dist/components/tabs/tabs.js +341 -0
- package/dist/components/tabs/tabs.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/keydown/keydown.d.ts +1 -0
- package/dist/interactions/keydown/keydown.js +8 -0
- package/dist/interactions/keydown/keydown.js.map +1 -0
- package/dist/interactions/outside-click/outside-click-mixin.d.ts +2 -0
- package/dist/interactions/outside-click/outside-click-mixin.js +29 -0
- package/dist/interactions/outside-click/outside-click-mixin.js.map +1 -0
- package/dist/interactions/typeahead/typeahead-mixin.d.ts +8 -0
- package/dist/interactions/typeahead/typeahead-mixin.js +59 -0
- package/dist/interactions/typeahead/typeahead-mixin.js.map +1 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +3 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/runtime/client-entries.d.ts +91 -0
- package/dist/runtime/client-entries.js +40 -0
- package/dist/runtime/client-entries.js.map +1 -0
- package/dist/runtime/component.d.ts +223 -0
- package/dist/runtime/component.js +152 -0
- package/dist/runtime/component.js.map +1 -0
- package/dist/runtime/create-element.d.ts +10 -0
- package/dist/runtime/create-element.js +29 -0
- package/dist/runtime/create-element.js.map +1 -0
- package/dist/runtime/diff-dom.d.ts +2 -0
- package/dist/runtime/diff-dom.js +364 -0
- package/dist/runtime/diff-dom.js.map +1 -0
- package/dist/runtime/diff-props.d.ts +9 -0
- package/dist/runtime/diff-props.js +195 -0
- package/dist/runtime/diff-props.js.map +1 -0
- package/dist/runtime/document-state.d.ts +11 -0
- package/dist/runtime/document-state.js +106 -0
- package/dist/runtime/document-state.js.map +1 -0
- package/dist/runtime/dom.d.ts +3189 -0
- package/dist/runtime/dom.js +2 -0
- package/dist/runtime/dom.js.map +1 -0
- package/dist/runtime/error-event.d.ts +20 -0
- package/dist/runtime/error-event.js +19 -0
- package/dist/runtime/error-event.js.map +1 -0
- package/dist/runtime/event-listeners.d.ts +50 -0
- package/dist/runtime/event-listeners.js +31 -0
- package/dist/runtime/event-listeners.js.map +1 -0
- package/dist/runtime/frame.d.ts +119 -0
- package/dist/runtime/frame.js +915 -0
- package/dist/runtime/frame.js.map +1 -0
- package/dist/runtime/invariant.d.ts +23 -0
- package/dist/runtime/invariant.js +33 -0
- package/dist/runtime/invariant.js.map +1 -0
- package/dist/runtime/jsx.d.ts +320 -0
- package/dist/runtime/jsx.js +31 -0
- package/dist/runtime/jsx.js.map +1 -0
- package/dist/runtime/mixins/attrs-mixin.d.ts +9 -0
- package/dist/runtime/mixins/attrs-mixin.js +23 -0
- package/dist/runtime/mixins/attrs-mixin.js.map +1 -0
- package/dist/runtime/mixins/link-mixin.d.ts +17 -0
- package/dist/runtime/mixins/link-mixin.js +101 -0
- package/dist/runtime/mixins/link-mixin.js.map +1 -0
- package/dist/runtime/mixins/mixin.d.ts +145 -0
- package/dist/runtime/mixins/mixin.js +570 -0
- package/dist/runtime/mixins/mixin.js.map +1 -0
- package/dist/runtime/mixins/on-mixin.d.ts +16 -0
- package/dist/runtime/mixins/on-mixin.js +49 -0
- package/dist/runtime/mixins/on-mixin.js.map +1 -0
- package/dist/runtime/mixins/ref-mixin.d.ts +9 -0
- package/dist/runtime/mixins/ref-mixin.js +21 -0
- package/dist/runtime/mixins/ref-mixin.js.map +1 -0
- package/dist/runtime/navigation.d.ts +22 -0
- package/dist/runtime/navigation.js +102 -0
- package/dist/runtime/navigation.js.map +1 -0
- package/dist/runtime/reconcile.d.ts +11 -0
- package/dist/runtime/reconcile.js +1240 -0
- package/dist/runtime/reconcile.js.map +1 -0
- package/dist/runtime/render.d.ts +54 -0
- package/dist/runtime/render.js +50 -0
- package/dist/runtime/render.js.map +1 -0
- package/dist/runtime/run.d.ts +45 -0
- package/dist/runtime/run.js +68 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/scheduler.d.ts +30 -0
- package/dist/runtime/scheduler.js +185 -0
- package/dist/runtime/scheduler.js.map +1 -0
- package/dist/runtime/svg-attributes.d.ts +5 -0
- package/dist/runtime/svg-attributes.js +124 -0
- package/dist/runtime/svg-attributes.js.map +1 -0
- package/dist/runtime/to-vnode.d.ts +3 -0
- package/dist/runtime/to-vnode.js +43 -0
- package/dist/runtime/to-vnode.js.map +1 -0
- package/dist/runtime/typed-event-target.d.ts +50 -0
- package/dist/runtime/typed-event-target.js +6 -0
- package/dist/runtime/typed-event-target.js.map +1 -0
- package/dist/runtime/vdom.d.ts +55 -0
- package/dist/runtime/vdom.js +198 -0
- package/dist/runtime/vdom.js.map +1 -0
- package/dist/runtime/vnode.d.ts +79 -0
- package/dist/runtime/vnode.js +38 -0
- package/dist/runtime/vnode.js.map +1 -0
- package/dist/server/stream.d.ts +57 -0
- package/dist/server/stream.js +1007 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/style/css-mixin.d.ts +8 -0
- package/dist/style/css-mixin.js +54 -0
- package/dist/style/css-mixin.js.map +1 -0
- package/dist/style/index.d.ts +5 -0
- package/dist/style/index.js +4 -0
- package/dist/style/index.js.map +1 -0
- package/dist/style/style.d.ts +24 -0
- package/dist/style/style.js +269 -0
- package/dist/style/style.js.map +1 -0
- package/dist/style/stylesheet.d.ts +11 -0
- package/dist/style/stylesheet.js +174 -0
- package/dist/style/stylesheet.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +2 -0
- package/dist/test.js.map +1 -0
- package/dist/theme/contract.d.ts +259 -0
- package/dist/theme/contract.js +131 -0
- package/dist/theme/contract.js.map +1 -0
- package/dist/theme/glyph-contract.d.ts +13 -0
- package/dist/theme/glyph-contract.js +34 -0
- package/dist/theme/glyph-contract.js.map +1 -0
- package/dist/theme/presets/rmx-01/glyphs.d.ts +2 -0
- package/dist/theme/presets/rmx-01/glyphs.js +252 -0
- package/dist/theme/presets/rmx-01/glyphs.js.map +1 -0
- package/dist/theme/presets/rmx-01/index.d.ts +2 -0
- package/dist/theme/presets/rmx-01/index.js +119 -0
- package/dist/theme/presets/rmx-01/index.js.map +1 -0
- package/dist/theme/runtime.d.ts +2 -0
- package/dist/theme/runtime.js +81 -0
- package/dist/theme/runtime.js.map +1 -0
- package/dist/theme/theme.d.ts +6 -0
- package/dist/theme/theme.js +5 -0
- package/dist/theme/theme.js.map +1 -0
- package/dist/utils/flash-attribute.d.ts +1 -0
- package/dist/utils/flash-attribute.js +11 -0
- package/dist/utils/flash-attribute.js.map +1 -0
- package/dist/utils/scroll-lock.d.ts +3 -0
- package/dist/utils/scroll-lock.js +69 -0
- package/dist/utils/scroll-lock.js.map +1 -0
- package/dist/utils/wait-for-css-transition.d.ts +1 -0
- package/dist/utils/wait-for-css-transition.js +58 -0
- package/dist/utils/wait-for-css-transition.js.map +1 -0
- package/dist/utils/wait.d.ts +1 -0
- package/dist/utils/wait.js +6 -0
- package/dist/utils/wait.js.map +1 -0
- package/package.json +125 -5
- package/src/animation/animate-layout-mixin.ts +266 -0
- package/src/animation/animate-mixins.ts +292 -0
- package/src/animation/index.ts +6 -0
- package/src/animation/spring.ts +299 -0
- package/src/animation/tween.ts +134 -0
- package/src/components/accordion/accordion.tsx +565 -0
- package/src/components/anchor/anchor.ts +904 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +124 -0
- package/src/components/button/README.md +44 -0
- package/src/components/button/button.tsx +188 -0
- package/src/components/combobox/README.md +145 -0
- package/src/components/combobox/combobox.tsx +1014 -0
- package/src/components/glyph/glyph.tsx +110 -0
- package/src/components/listbox/listbox.ts +447 -0
- package/src/components/menu/hover-aim.ts +428 -0
- package/src/components/menu/menu.tsx +1547 -0
- package/src/components/popover/README.md +122 -0
- package/src/components/popover/popover.ts +201 -0
- package/src/components/select/select.tsx +585 -0
- package/src/components/separator/separator.ts +19 -0
- package/src/components/tabs/README.md +105 -0
- package/src/components/tabs/tabs.tsx +499 -0
- package/src/index.ts +72 -0
- package/src/interactions/keydown/keydown.ts +14 -0
- package/src/interactions/outside-click/outside-click-mixin.ts +55 -0
- package/src/interactions/typeahead/typeahead-mixin.ts +89 -0
- package/src/jsx-runtime.ts +2 -0
- package/src/runtime/client-entries.ts +137 -0
- package/src/runtime/component.ts +402 -0
- package/src/runtime/create-element.ts +38 -0
- package/src/runtime/diff-dom.ts +404 -0
- package/src/runtime/diff-props.ts +209 -0
- package/src/runtime/document-state.ts +130 -0
- package/src/runtime/dom.ts +3971 -0
- package/src/runtime/error-event.ts +26 -0
- package/src/runtime/event-listeners.ts +171 -0
- package/src/runtime/frame.ts +1211 -0
- package/src/runtime/invariant.ts +31 -0
- package/src/runtime/jsx.ts +398 -0
- package/src/runtime/mixins/attrs-mixin.ts +35 -0
- package/src/runtime/mixins/link-mixin.ts +131 -0
- package/src/runtime/mixins/mixin.ts +908 -0
- package/src/runtime/mixins/on-mixin.ts +89 -0
- package/src/runtime/mixins/ref-mixin.ts +32 -0
- package/src/runtime/navigation.ts +136 -0
- package/src/runtime/reconcile.ts +1722 -0
- package/src/runtime/render.ts +101 -0
- package/src/runtime/run.ts +100 -0
- package/src/runtime/scheduler.ts +262 -0
- package/src/runtime/svg-attributes.ts +133 -0
- package/src/runtime/to-vnode.ts +50 -0
- package/src/runtime/typed-event-target.ts +67 -0
- package/src/runtime/vdom.ts +295 -0
- package/src/runtime/vnode.ts +137 -0
- package/src/server/stream.ts +1342 -0
- package/src/style/css-mixin.ts +82 -0
- package/src/style/index.ts +10 -0
- package/src/style/style.ts +312 -0
- package/src/style/stylesheet.ts +209 -0
- package/src/test/setup.ts +7 -0
- package/src/test/utils.ts +45 -0
- package/src/test.ts +2 -0
- package/src/theme/contract.ts +183 -0
- package/src/theme/glyph-contract.ts +57 -0
- package/src/theme/presets/rmx-01/glyphs.tsx +308 -0
- package/src/theme/presets/rmx-01/index.ts +121 -0
- package/src/theme/runtime.ts +116 -0
- package/src/theme/theme.ts +15 -0
- package/src/utils/flash-attribute.ts +11 -0
- package/src/utils/scroll-lock.ts +97 -0
- package/src/utils/wait-for-css-transition.ts +66 -0
- 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
|
+
}
|