@remix-run/ui 0.2.0 → 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.
- package/dist/animation/demos/animation.demo.d.ts +5 -0
- package/dist/animation/demos/animation.demo.js +92 -0
- package/dist/animation/demos/animation.demo.js.map +1 -0
- package/dist/animation/demos/aspect-ratio.d.ts +2 -0
- package/dist/animation/demos/aspect-ratio.js +69 -0
- package/dist/animation/demos/aspect-ratio.js.map +1 -0
- package/dist/animation/demos/bouncy-switch.d.ts +2 -0
- package/dist/animation/demos/bouncy-switch.js +36 -0
- package/dist/animation/demos/bouncy-switch.js.map +1 -0
- package/dist/animation/demos/color-interpolation.d.ts +1 -0
- package/dist/animation/demos/color-interpolation.js +53 -0
- package/dist/animation/demos/color-interpolation.js.map +1 -0
- package/dist/animation/demos/cube.d.ts +2 -0
- package/dist/animation/demos/cube.js +86 -0
- package/dist/animation/demos/cube.js.map +1 -0
- package/dist/animation/demos/default-animate.d.ts +2 -0
- package/dist/animation/demos/default-animate.js +89 -0
- package/dist/animation/demos/default-animate.js.map +1 -0
- package/dist/animation/demos/drag-release.d.ts +24 -0
- package/dist/animation/demos/drag-release.js +96 -0
- package/dist/animation/demos/drag-release.js.map +1 -0
- package/dist/animation/demos/enter.d.ts +1 -0
- package/dist/animation/demos/enter.js +19 -0
- package/dist/animation/demos/enter.js.map +1 -0
- package/dist/animation/demos/exit.d.ts +2 -0
- package/dist/animation/demos/exit.js +58 -0
- package/dist/animation/demos/exit.js.map +1 -0
- package/dist/animation/demos/flip-toggle.d.ts +2 -0
- package/dist/animation/demos/flip-toggle.js +37 -0
- package/dist/animation/demos/flip-toggle.js.map +1 -0
- package/dist/animation/demos/hold-to-confirm.d.ts +13 -0
- package/dist/animation/demos/hold-to-confirm.js +211 -0
- package/dist/animation/demos/hold-to-confirm.js.map +1 -0
- package/dist/animation/demos/html-content.d.ts +2 -0
- package/dist/animation/demos/html-content.js +30 -0
- package/dist/animation/demos/html-content.js.map +1 -0
- package/dist/animation/demos/interruptible-keyframes.d.ts +1 -0
- package/dist/animation/demos/interruptible-keyframes.js +48 -0
- package/dist/animation/demos/interruptible-keyframes.js.map +1 -0
- package/dist/animation/demos/keyframes.d.ts +1 -0
- package/dist/animation/demos/keyframes.js +24 -0
- package/dist/animation/demos/keyframes.js.map +1 -0
- package/dist/animation/demos/material-ripple.d.ts +2 -0
- package/dist/animation/demos/material-ripple.js +127 -0
- package/dist/animation/demos/material-ripple.js.map +1 -0
- package/dist/animation/demos/mixin-presence-list.d.ts +2 -0
- package/dist/animation/demos/mixin-presence-list.js +88 -0
- package/dist/animation/demos/mixin-presence-list.js.map +1 -0
- package/dist/animation/demos/mixin-reclaim.d.ts +2 -0
- package/dist/animation/demos/mixin-reclaim.js +90 -0
- package/dist/animation/demos/mixin-reclaim.js.map +1 -0
- package/dist/animation/demos/multi-state-badge.d.ts +2 -0
- package/dist/animation/demos/multi-state-badge.js +231 -0
- package/dist/animation/demos/multi-state-badge.js.map +1 -0
- package/dist/animation/demos/press.d.ts +2 -0
- package/dist/animation/demos/press.js +63 -0
- package/dist/animation/demos/press.js.map +1 -0
- package/dist/animation/demos/reordering.d.ts +2 -0
- package/dist/animation/demos/reordering.js +50 -0
- package/dist/animation/demos/reordering.js.map +1 -0
- package/dist/animation/demos/rolling-square.d.ts +2 -0
- package/dist/animation/demos/rolling-square.js +42 -0
- package/dist/animation/demos/rolling-square.js.map +1 -0
- package/dist/animation/demos/rotate.d.ts +1 -0
- package/dist/animation/demos/rotate.js +18 -0
- package/dist/animation/demos/rotate.js.map +1 -0
- package/dist/animation/demos/shared-layout.d.ts +2 -0
- package/dist/animation/demos/shared-layout.js +114 -0
- package/dist/animation/demos/shared-layout.js.map +1 -0
- package/dist/animation/demos/spring.demo.d.ts +6 -0
- package/dist/animation/demos/spring.demo.js +304 -0
- package/dist/animation/demos/spring.demo.js.map +1 -0
- package/dist/animation/demos/transition-options.d.ts +1 -0
- package/dist/animation/demos/transition-options.js +21 -0
- package/dist/animation/demos/transition-options.js.map +1 -0
- package/dist/animation/spring.d.ts +22 -0
- package/dist/animation/spring.js +22 -0
- package/dist/animation/spring.js.map +1 -1
- package/dist/components/accordion/demos/card.demo.d.ts +1 -0
- package/dist/components/accordion/demos/card.demo.js +1 -0
- package/dist/components/accordion/demos/card.demo.js.map +1 -1
- package/dist/components/accordion/demos/multiple.demo.d.ts +1 -0
- package/dist/components/accordion/demos/multiple.demo.js +1 -0
- package/dist/components/accordion/demos/multiple.demo.js.map +1 -1
- package/dist/components/accordion/demos/overview.demo.d.ts +1 -0
- package/dist/components/accordion/demos/overview.demo.js +1 -0
- package/dist/components/accordion/demos/overview.demo.js.map +1 -1
- package/dist/components/anchor/anchor.d.ts +8 -1
- package/dist/components/anchor/anchor.js +30 -11
- package/dist/components/anchor/anchor.js.map +1 -1
- package/dist/components/breadcrumbs/{demos/basic.demo.d.ts → basic.demo.d.ts} +1 -0
- package/dist/components/breadcrumbs/{demos/basic.demo.js → basic.demo.js} +1 -0
- package/dist/components/breadcrumbs/basic.demo.js.map +1 -0
- package/dist/components/breadcrumbs/{demos/separator.demo.d.ts → separator.demo.d.ts} +1 -0
- package/dist/components/breadcrumbs/{demos/separator.demo.js → separator.demo.js} +1 -0
- package/dist/components/breadcrumbs/separator.demo.js.map +1 -0
- package/dist/components/button/demos/aliases.demo.d.ts +1 -0
- package/dist/components/button/demos/aliases.demo.js +1 -0
- package/dist/components/button/demos/aliases.demo.js.map +1 -1
- package/dist/components/button/demos/basic.demo.d.ts +1 -0
- package/dist/components/button/demos/basic.demo.js +1 -0
- package/dist/components/button/demos/basic.demo.js.map +1 -1
- package/dist/components/button/demos/states.demo.d.ts +1 -0
- package/dist/components/button/demos/states.demo.js +1 -0
- package/dist/components/button/demos/states.demo.js.map +1 -1
- package/dist/components/combobox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
- package/dist/components/combobox/{demos/overview.demo.js → overview.demo.js} +1 -0
- package/dist/components/combobox/overview.demo.js.map +1 -0
- package/dist/components/listbox/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
- package/dist/components/listbox/{demos/overview.demo.js → overview.demo.js} +1 -0
- package/dist/components/listbox/overview.demo.js.map +1 -0
- package/dist/components/menu/demos/bubbling.demo.d.ts +1 -0
- package/dist/components/menu/demos/bubbling.demo.js +1 -0
- package/dist/components/menu/demos/bubbling.demo.js.map +1 -1
- package/dist/components/menu/demos/context-trigger.demo.d.ts +7 -0
- package/dist/components/menu/demos/context-trigger.demo.js +87 -0
- package/dist/components/menu/demos/context-trigger.demo.js.map +1 -0
- package/dist/components/menu/demos/overview.demo.d.ts +1 -0
- package/dist/components/menu/demos/overview.demo.js +1 -0
- package/dist/components/menu/demos/overview.demo.js.map +1 -1
- package/dist/components/menu/menu.d.ts +3 -0
- package/dist/components/menu/menu.js +55 -0
- package/dist/components/menu/menu.js.map +1 -1
- package/dist/components/popover/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
- package/dist/components/popover/{demos/overview.demo.js → overview.demo.js} +1 -0
- package/dist/components/popover/overview.demo.js.map +1 -0
- package/dist/components/popover/popover.d.ts +2 -2
- package/dist/components/popover/popover.js +11 -4
- package/dist/components/popover/popover.js.map +1 -1
- package/dist/components/select/{demos/deconstructed.demo.d.ts → deconstructed.demo.d.ts} +1 -0
- package/dist/components/select/{demos/deconstructed.demo.js → deconstructed.demo.js} +1 -0
- package/dist/components/select/deconstructed.demo.js.map +1 -0
- package/dist/components/select/{demos/overview.demo.d.ts → overview.demo.d.ts} +1 -0
- package/dist/components/select/{demos/overview.demo.js → overview.demo.js} +1 -0
- package/dist/components/select/overview.demo.js.map +1 -0
- package/dist/components/select/select.js +1 -1
- package/dist/components/select/select.js.map +1 -1
- package/dist/runtime/client-entries.d.ts +1 -2
- package/dist/runtime/client-entries.js.map +1 -1
- package/dist/runtime/component.d.ts +2 -2
- package/dist/runtime/component.js +1 -1
- package/dist/runtime/component.js.map +1 -1
- package/dist/runtime/demos/basic.demo.d.ts +6 -0
- package/dist/runtime/demos/basic.demo.js +16 -0
- package/dist/runtime/demos/basic.demo.js.map +1 -0
- package/dist/runtime/demos/controlled-uncontrolled-values.demo.d.ts +6 -0
- package/dist/runtime/demos/controlled-uncontrolled-values.demo.js +77 -0
- package/dist/runtime/demos/controlled-uncontrolled-values.demo.js.map +1 -0
- package/dist/runtime/demos/keyed-list.demo.d.ts +6 -0
- package/dist/runtime/demos/keyed-list.demo.js +62 -0
- package/dist/runtime/demos/keyed-list.demo.js.map +1 -0
- package/dist/runtime/demos/readme.demo.d.ts +5 -0
- package/dist/runtime/demos/readme.demo.js +396 -0
- package/dist/runtime/demos/readme.demo.js.map +1 -0
- package/dist/runtime/diff-dom.js +194 -21
- package/dist/runtime/diff-dom.js.map +1 -1
- package/dist/runtime/frame.d.ts +7 -0
- package/dist/runtime/frame.js +176 -41
- package/dist/runtime/frame.js.map +1 -1
- package/dist/runtime/jsx.d.ts +2 -3
- package/dist/runtime/jsx.js.map +1 -1
- package/dist/server/stream.js +3 -2
- package/dist/server/stream.js.map +1 -1
- package/dist/style/style.js +27 -10
- package/dist/style/style.js.map +1 -1
- package/package.json +3 -3
- package/src/animation/demos/animation.demo.tsx +194 -0
- package/src/animation/demos/aspect-ratio.tsx +120 -0
- package/src/animation/demos/bouncy-switch.tsx +47 -0
- package/src/animation/demos/color-interpolation.tsx +73 -0
- package/src/animation/demos/cube.tsx +116 -0
- package/src/animation/demos/default-animate.tsx +129 -0
- package/src/animation/demos/drag-release.ts +118 -0
- package/src/animation/demos/enter.tsx +22 -0
- package/src/animation/demos/exit.tsx +79 -0
- package/src/animation/demos/flip-toggle.tsx +46 -0
- package/src/animation/demos/hold-to-confirm.tsx +317 -0
- package/src/animation/demos/html-content.tsx +41 -0
- package/src/animation/demos/interruptible-keyframes.tsx +62 -0
- package/src/animation/demos/keyframes.tsx +29 -0
- package/src/animation/demos/material-ripple.tsx +158 -0
- package/src/animation/demos/mixin-presence-list.tsx +121 -0
- package/src/animation/demos/mixin-reclaim.tsx +125 -0
- package/src/animation/demos/multi-state-badge.tsx +426 -0
- package/src/animation/demos/press.tsx +67 -0
- package/src/animation/demos/reordering.tsx +65 -0
- package/src/animation/demos/rolling-square.tsx +57 -0
- package/src/animation/demos/rotate.tsx +21 -0
- package/src/animation/demos/shared-layout.tsx +211 -0
- package/src/animation/demos/spring.demo.tsx +385 -0
- package/src/animation/demos/transition-options.tsx +24 -0
- package/src/animation/spring.ts +22 -0
- package/src/components/accordion/demos/card.demo.tsx +1 -0
- package/src/components/accordion/demos/multiple.demo.tsx +1 -0
- package/src/components/accordion/demos/overview.demo.tsx +1 -0
- package/src/components/anchor/README.md +11 -3
- package/src/components/anchor/anchor.ts +53 -12
- package/src/components/breadcrumbs/{demos/basic.demo.tsx → basic.demo.tsx} +1 -0
- package/src/components/breadcrumbs/{demos/separator.demo.tsx → separator.demo.tsx} +1 -0
- package/src/components/button/demos/aliases.demo.tsx +1 -0
- package/src/components/button/demos/basic.demo.tsx +1 -0
- package/src/components/button/demos/states.demo.tsx +1 -0
- package/src/components/combobox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
- package/src/components/listbox/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
- package/src/components/menu/README.md +26 -2
- package/src/components/menu/demos/bubbling.demo.tsx +1 -0
- package/src/components/menu/demos/context-trigger.demo.tsx +133 -0
- package/src/components/menu/demos/overview.demo.tsx +1 -0
- package/src/components/menu/menu.tsx +73 -1
- package/src/components/popover/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
- package/src/components/popover/popover.ts +18 -9
- package/src/components/select/{demos/deconstructed.demo.tsx → deconstructed.demo.tsx} +1 -0
- package/src/components/select/{demos/overview.demo.tsx → overview.demo.tsx} +1 -0
- package/src/components/select/select.tsx +1 -1
- package/src/runtime/client-entries.ts +1 -5
- package/src/runtime/component.ts +3 -3
- package/src/runtime/demos/basic.demo.tsx +21 -0
- package/src/runtime/demos/controlled-uncontrolled-values.demo.tsx +177 -0
- package/src/runtime/demos/keyed-list.demo.tsx +94 -0
- package/src/runtime/demos/readme.demo.tsx +702 -0
- package/src/runtime/diff-dom.ts +270 -20
- package/src/runtime/frame.ts +218 -41
- package/src/runtime/jsx.ts +6 -10
- package/src/server/stream.ts +7 -2
- package/src/style/style.ts +29 -10
- package/dist/components/breadcrumbs/demos/basic.demo.js.map +0 -1
- package/dist/components/breadcrumbs/demos/separator.demo.js.map +0 -1
- package/dist/components/combobox/demos/overview.demo.js.map +0 -1
- package/dist/components/listbox/demos/overview.demo.js.map +0 -1
- package/dist/components/popover/demos/overview.demo.js.map +0 -1
- package/dist/components/select/demos/deconstructed.demo.js.map +0 -1
- package/dist/components/select/demos/overview.demo.js.map +0 -1
|
@@ -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
|
+
}
|
package/src/animation/spring.ts
CHANGED
|
@@ -144,6 +144,28 @@ function transition(
|
|
|
144
144
|
return properties.map((p) => `${p} ${s}`).join(', ')
|
|
145
145
|
}
|
|
146
146
|
|
|
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
|
+
*/
|
|
147
169
|
export const spring: SpringFunction = Object.assign(createSpring, {
|
|
148
170
|
transition,
|
|
149
171
|
presets,
|
|
@@ -10,6 +10,7 @@ import { theme } from '@remix-run/ui/theme'
|
|
|
10
10
|
/**
|
|
11
11
|
* @name Accordion in a Card
|
|
12
12
|
* @description The Accordion can sit inside a card when the surrounding surface needs stronger grouping.
|
|
13
|
+
* @layout center
|
|
13
14
|
* @order 3
|
|
14
15
|
*/
|
|
15
16
|
export default function Example() {
|
|
@@ -11,6 +11,7 @@ import { theme } from '@remix-run/ui/theme'
|
|
|
11
11
|
* @order 2
|
|
12
12
|
* @name Accordion Multiple Open
|
|
13
13
|
* @description Multiple mode allows several sections to stay open simultaneously, useful for operational checklists and dashboards.
|
|
14
|
+
* @layout center
|
|
14
15
|
*/
|
|
15
16
|
export default function Example() {
|
|
16
17
|
return () => (
|
|
@@ -10,6 +10,7 @@ import { theme } from '@remix-run/ui/theme'
|
|
|
10
10
|
/**
|
|
11
11
|
* @name Accordion Overview
|
|
12
12
|
* @description A single-open disclosure list that keeps settings, billing, or notification rules in one calm section.
|
|
13
|
+
* @layout center
|
|
13
14
|
* @order 1
|
|
14
15
|
*/
|
|
15
16
|
export default function Example() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# anchor
|
|
2
2
|
|
|
3
|
-
`anchor` positions a floating element against an anchor element and keeps it constrained to the viewport. Use it for custom floating surfaces that need placement, flipping, offsets, and optional relative alignment.
|
|
3
|
+
`anchor` positions a floating element against an anchor element or viewport coordinates and keeps it constrained to the viewport. Use it for custom floating surfaces that need placement, flipping, offsets, and optional relative alignment.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -49,11 +49,19 @@ popover.addEventListener('beforetoggle', (event) => {
|
|
|
49
49
|
})
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
Anchor to coordinates when the surface should open at a pointer location.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
let cleanup = anchor(popover, { x: event.clientX, y: event.clientY }, { placement: 'bottom-start' })
|
|
56
|
+
```
|
|
57
|
+
|
|
52
58
|
## `anchor.*`
|
|
53
59
|
|
|
54
|
-
- `anchor(floatingElement,
|
|
60
|
+
- `anchor(floatingElement, anchorTarget, options)`: positions `floatingElement` against an element or coordinate target, starts animation-frame polling for geometry changes, and returns a cleanup function.
|
|
55
61
|
- `AnchorOptions`: placement, inset, relative alignment, and offset options.
|
|
62
|
+
- `AnchorPoint`: viewport coordinate target with `x`, `y`, and optional `width`/`height`.
|
|
56
63
|
- `AnchorPlacement`: exported placement names for the main sides and top/bottom start/end alignment.
|
|
64
|
+
- `AnchorTarget`: an `HTMLElement` or `AnchorPoint`.
|
|
57
65
|
|
|
58
66
|
## Placements
|
|
59
67
|
|
|
@@ -149,5 +157,5 @@ anchor(listbox, trigger, {
|
|
|
149
157
|
- Oversized inset surfaces with `relativeTo` preserve alignment by scrolling the nearest scrollable descendant when possible.
|
|
150
158
|
- `offset`, `offsetX`, and `offsetY` may be numbers or functions that receive the floating element.
|
|
151
159
|
- `relativeTo` lets a surface align to an inner element, which is useful for selected options inside popovers.
|
|
152
|
-
- `anchor` polls on animation frames for anchor or floating geometry changes and repositions when either changes.
|
|
160
|
+
- `anchor` polls on animation frames for anchor target or floating geometry changes and repositions when either changes.
|
|
153
161
|
- The returned cleanup function cancels animation-frame polling.
|
|
@@ -24,6 +24,15 @@ export type AnchorPlacement =
|
|
|
24
24
|
|
|
25
25
|
type AnchorOffsetValue = number | ((floating: HTMLElement) => number)
|
|
26
26
|
|
|
27
|
+
export interface AnchorPoint {
|
|
28
|
+
height?: number
|
|
29
|
+
width?: number
|
|
30
|
+
x: number
|
|
31
|
+
y: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AnchorTarget = HTMLElement | AnchorPoint
|
|
35
|
+
|
|
27
36
|
export type AnchorOptions = {
|
|
28
37
|
placement?: ExtendedAnchorPlacement
|
|
29
38
|
inset?: boolean
|
|
@@ -125,6 +134,39 @@ function resolveOffsetValue(offset: AnchorOffsetValue | undefined, floating: HTM
|
|
|
125
134
|
return offset ?? 0
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
function isAnchorPoint(value: unknown): value is AnchorPoint {
|
|
138
|
+
if (typeof value !== 'object' || value === null) {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!('x' in value) || !('y' in value)) {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let width = 'width' in value ? value.width : undefined
|
|
147
|
+
let height = 'height' in value ? value.height : undefined
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
typeof value.x === 'number' &&
|
|
151
|
+
typeof value.y === 'number' &&
|
|
152
|
+
(width === undefined || typeof width === 'number') &&
|
|
153
|
+
(height === undefined || typeof height === 'number')
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readAnchorRect(anchorTarget: AnchorTarget) {
|
|
158
|
+
if (anchorTarget instanceof HTMLElement) {
|
|
159
|
+
return anchorTarget.getBoundingClientRect()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return new DOMRect(
|
|
163
|
+
anchorTarget.x,
|
|
164
|
+
anchorTarget.y,
|
|
165
|
+
anchorTarget.width ?? 0,
|
|
166
|
+
anchorTarget.height ?? 0,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
128
170
|
function calculatePosition(
|
|
129
171
|
placement: ExtendedAnchorPlacement,
|
|
130
172
|
anchorRect: DOMRect,
|
|
@@ -468,7 +510,7 @@ function getFlippedPlacement(
|
|
|
468
510
|
placementHeight: number,
|
|
469
511
|
collisionWidth: number,
|
|
470
512
|
collisionHeight: number,
|
|
471
|
-
|
|
513
|
+
anchorIsAbsolutePositioned: boolean,
|
|
472
514
|
inset: boolean,
|
|
473
515
|
offset: number,
|
|
474
516
|
offsetX: number,
|
|
@@ -476,11 +518,9 @@ function getFlippedPlacement(
|
|
|
476
518
|
relativeOffsetX?: number | null,
|
|
477
519
|
relativeOffsetY?: number | null,
|
|
478
520
|
) {
|
|
479
|
-
let computedStyle = getComputedStyle(anchor)
|
|
480
|
-
let isAbsolutePositioned = computedStyle.position !== 'fixed'
|
|
481
521
|
let hasScroll = window.scrollY > 0
|
|
482
522
|
|
|
483
|
-
if (placement.startsWith('top') &&
|
|
523
|
+
if (placement.startsWith('top') && anchorIsAbsolutePositioned && hasScroll) {
|
|
484
524
|
if (anchorRect.top - placementHeight < 0) {
|
|
485
525
|
return placement
|
|
486
526
|
}
|
|
@@ -641,7 +681,7 @@ function solveRelativeAlignmentAxis(options: {
|
|
|
641
681
|
|
|
642
682
|
export function anchor(
|
|
643
683
|
floating: HTMLElement,
|
|
644
|
-
|
|
684
|
+
anchorTarget: AnchorTarget,
|
|
645
685
|
options: AnchorOptions = {},
|
|
646
686
|
): () => void {
|
|
647
687
|
let lastAnchorRect: DOMRect
|
|
@@ -651,8 +691,8 @@ export function anchor(
|
|
|
651
691
|
throw new TypeError('anchor() expected a floating HTMLElement')
|
|
652
692
|
}
|
|
653
693
|
|
|
654
|
-
if (!(
|
|
655
|
-
throw new TypeError('anchor() expected an anchor HTMLElement')
|
|
694
|
+
if (!(anchorTarget instanceof HTMLElement) && !isAnchorPoint(anchorTarget)) {
|
|
695
|
+
throw new TypeError('anchor() expected an anchor HTMLElement or coordinates')
|
|
656
696
|
}
|
|
657
697
|
|
|
658
698
|
let {
|
|
@@ -664,12 +704,13 @@ export function anchor(
|
|
|
664
704
|
offsetY: rawOffsetY = 0,
|
|
665
705
|
} = options
|
|
666
706
|
|
|
667
|
-
let
|
|
668
|
-
|
|
707
|
+
let anchorIsAbsolutePositioned =
|
|
708
|
+
anchorTarget instanceof HTMLElement && getComputedStyle(anchorTarget).position !== 'fixed'
|
|
709
|
+
let isFixed = floating.hasAttribute('popover') || !anchorIsAbsolutePositioned
|
|
669
710
|
let animationFrameId = 0
|
|
670
711
|
|
|
671
712
|
function updatePosition(
|
|
672
|
-
anchorRect =
|
|
713
|
+
anchorRect = readAnchorRect(anchorTarget),
|
|
673
714
|
naturalDimensions = getFloatingDimensions(floating, relativeTo, {
|
|
674
715
|
ignoreInlineMaxSize: true,
|
|
675
716
|
}),
|
|
@@ -695,7 +736,7 @@ export function anchor(
|
|
|
695
736
|
placementHeight,
|
|
696
737
|
naturalDimensions.width,
|
|
697
738
|
naturalDimensions.height,
|
|
698
|
-
|
|
739
|
+
anchorIsAbsolutePositioned,
|
|
699
740
|
inset,
|
|
700
741
|
offset,
|
|
701
742
|
offsetX,
|
|
@@ -872,7 +913,7 @@ export function anchor(
|
|
|
872
913
|
updatePosition()
|
|
873
914
|
|
|
874
915
|
function pollForPositionChanges() {
|
|
875
|
-
let currentRect =
|
|
916
|
+
let currentRect = readAnchorRect(anchorTarget)
|
|
876
917
|
let currentDimensions = getFloatingDimensions(floating, relativeTo)
|
|
877
918
|
if (
|
|
878
919
|
hasRectChanged(currentRect, lastAnchorRect) ||
|
|
@@ -5,6 +5,7 @@ import { theme } from '@remix-run/ui/theme'
|
|
|
5
5
|
/**
|
|
6
6
|
* @name Button Component
|
|
7
7
|
* @description The Button component wraps the low-level style primitives and accepts a tone prop for quick theming.
|
|
8
|
+
* @layout center
|
|
8
9
|
* @order 2
|
|
9
10
|
*/
|
|
10
11
|
export default function Example() {
|
|
@@ -5,6 +5,7 @@ import { theme } from '@remix-run/ui/theme'
|
|
|
5
5
|
/**
|
|
6
6
|
* @name Combobox Overview
|
|
7
7
|
* @description A searchable combobox with keyboard navigation. Try typing airport names or codes like ord, lax, or jfk.
|
|
8
|
+
* @layout center
|
|
8
9
|
*/
|
|
9
10
|
export default function Example(handle: Handle) {
|
|
10
11
|
let value: string | null = null
|
|
@@ -7,6 +7,7 @@ import { theme } from '@remix-run/ui/theme'
|
|
|
7
7
|
/**
|
|
8
8
|
* @name Listbox Overview
|
|
9
9
|
* @description A keyboard-navigable listbox with selection, highlighting, and an optional flash-selection animation.
|
|
10
|
+
* @layout center
|
|
10
11
|
*/
|
|
11
12
|
export default function Example(handle: Handle) {
|
|
12
13
|
let value: ListboxValue = options[0]!.value
|