@solidrt/components 0.0.46 → 0.0.47
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/AGENTS.md +5 -3
- package/package.json +2 -2
- package/src/press.ts +6 -6
- package/src/scroll-view.tsx +1 -2
- package/src/slider.tsx +3 -4
- package/src/arena.ts +0 -58
- package/src/pan.ts +0 -105
package/AGENTS.md
CHANGED
|
@@ -46,9 +46,11 @@ Most components group props into two objects, plus top-level event handlers:
|
|
|
46
46
|
- `ScrollView` - scrollable region; vertical by default, `horizontal` to flip.
|
|
47
47
|
Wheel + drag (a pan recognizer: activates on slop along the scroll axis and
|
|
48
48
|
steals the pointer from a pressable the drag started on), no momentum yet.
|
|
49
|
-
Backed by `createScroll` from `@solidrt/core` (headless
|
|
50
|
-
geometry
|
|
51
|
-
|
|
49
|
+
Backed by `createScroll` and `createPan` from `@solidrt/core` (headless
|
|
50
|
+
offset+clamp geometry; the recognizers and their gesture arena live in core
|
|
51
|
+
so every package arbitrates in the one app-wide arena). The press
|
|
52
|
+
recognizer (press.ts) stays in this package: per-pointer claims, innermost
|
|
53
|
+
press wins, pan steals on slop.
|
|
52
54
|
- `Pressable` - pressable box; `onPress` on a primary press released inside,
|
|
53
55
|
`disabled` opts out. `children`/`style` can be functions of `{ pressed,
|
|
54
56
|
hovered }`. Press retention: a drag out retracts the pressed state, a drag
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@solidjs/signals": "2.0.0-beta.31",
|
|
21
|
-
"@solidrt/core": "0.0.
|
|
21
|
+
"@solidrt/core": "0.0.47"
|
|
22
22
|
}
|
|
23
23
|
}
|
package/src/press.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createSignal, createMemo, onSettled, focusedNode, getBoundingBoxViewport } from "@solidrt/core"
|
|
1
|
+
import { arena, createSignal, createMemo, onSettled, focusedNode, getBoundingBoxViewport } from "@solidrt/core"
|
|
2
2
|
import type { PointerEvent, KeyEvent } from "@solidrt/core"
|
|
3
|
-
import { claim, release } from "./arena"
|
|
4
3
|
import { registerNavAction } from "./focus-nav"
|
|
5
4
|
|
|
6
5
|
// A live view of a recognizer's state, not a snapshot: the fields are getters,
|
|
@@ -46,8 +45,9 @@ export interface PressOptions {
|
|
|
46
45
|
// ref also registers onPress as the node's nav action, the path a
|
|
47
46
|
// controller's south button activates through (see focus-nav.ts). Key
|
|
48
47
|
// activation shows no pressed state - the focus ring is the feedback.
|
|
49
|
-
//
|
|
50
|
-
//
|
|
48
|
+
// The arena and the movement recognizers (createPan, createTransform) live in
|
|
49
|
+
// core; press stays here because it couples to this package's focus-nav
|
|
50
|
+
// (registerNavAction) and nothing outside components needs it yet
|
|
51
51
|
// (okf/plans/component-gestures.md).
|
|
52
52
|
export function createPress(options: PressOptions) {
|
|
53
53
|
let [pressed, setPressed] = createSignal(false)
|
|
@@ -107,7 +107,7 @@ export function createPress(options: PressOptions) {
|
|
|
107
107
|
|
|
108
108
|
let disengage = () => {
|
|
109
109
|
if (active != null) {
|
|
110
|
-
release(active, owner)
|
|
110
|
+
arena.release(active, owner)
|
|
111
111
|
active = null
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -127,7 +127,7 @@ export function createPress(options: PressOptions) {
|
|
|
127
127
|
let handlers = {
|
|
128
128
|
onPointerDown: (e: PointerEvent) => {
|
|
129
129
|
if (e.button != null && e.button !== 0) return
|
|
130
|
-
if (active == null && claim(e.pointerId, owner)) {
|
|
130
|
+
if (active == null && arena.claim(e.pointerId, owner)) {
|
|
131
131
|
active = e.pointerId
|
|
132
132
|
inside = true
|
|
133
133
|
setPressed(true)
|
package/src/scroll-view.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createScroll } from "@solidrt/core"
|
|
1
|
+
import { createPan, createScroll } from "@solidrt/core"
|
|
2
2
|
import type { LayoutProps, PointerProps, WheelEvent } from "@solidrt/core"
|
|
3
|
-
import { createPan } from "./pan"
|
|
4
3
|
import type { StyleProps } from "./types"
|
|
5
4
|
|
|
6
5
|
export interface ScrollViewProps extends PointerProps {
|
package/src/slider.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createSignal, getBoundingBox, onLayout, onSettled } from "@solidrt/core"
|
|
1
|
+
import { arena, createSignal, getBoundingBox, onLayout, onSettled } from "@solidrt/core"
|
|
2
2
|
import type { LayoutProps, PointerEvent } from "@solidrt/core"
|
|
3
|
-
import { release, steal } from "./arena"
|
|
4
3
|
import { theme } from "./theme"
|
|
5
4
|
import { densityScale } from "./policy"
|
|
6
5
|
import type { StyleProps } from "./types"
|
|
@@ -71,7 +70,7 @@ export function Slider(props: SliderProps) {
|
|
|
71
70
|
|
|
72
71
|
let endDrag = () => {
|
|
73
72
|
if (active != null) {
|
|
74
|
-
release(active, owner)
|
|
73
|
+
arena.release(active, owner)
|
|
75
74
|
active = null
|
|
76
75
|
}
|
|
77
76
|
}
|
|
@@ -84,7 +83,7 @@ export function Slider(props: SliderProps) {
|
|
|
84
83
|
if (props.disabled || active != null) return
|
|
85
84
|
// A down on the track is unambiguously a slider drag: resolve the arena
|
|
86
85
|
// outright so an ancestor scroller's pan cannot take the pointer over.
|
|
87
|
-
steal(e.pointerId, owner)
|
|
86
|
+
arena.steal(e.pointerId, owner)
|
|
88
87
|
active = e.pointerId
|
|
89
88
|
setFromClientX(e.clientX)
|
|
90
89
|
}
|
package/src/arena.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// The gesture arena: per-pointer ownership arbitration between recognizers.
|
|
2
|
-
// Raw pointer events keep bubbling along the frozen down path regardless; the
|
|
3
|
-
// arena only decides which recognizer's gesture a pointer belongs to.
|
|
4
|
-
//
|
|
5
|
-
// Two claim strengths mirror how gestures resolve. A press claims its pointer
|
|
6
|
-
// provisionally on the down: it is the presumed winner (innermost-wins falls
|
|
7
|
-
// out of leaf-to-root dispatch, first claim sticks) but a later recognizer
|
|
8
|
-
// with positive evidence of a different gesture (a pan crossing its movement
|
|
9
|
-
// slop) may steal the pointer, cancelling the press. A steal resolves the
|
|
10
|
-
// arena: the pointer is won outright and cannot be stolen again, so e.g. the
|
|
11
|
-
// outer axis of two nested scrollers cannot take a drag the inner one already
|
|
12
|
-
// owns. Plain state on purpose: claims must be visible across recognizers
|
|
13
|
-
// within one synchronous bubble dispatch, before any signal flush.
|
|
14
|
-
//
|
|
15
|
-
// Deliberately framework-agnostic (no theme, no styling, no components): a
|
|
16
|
-
// candidate for promotion into core along with the recognizers
|
|
17
|
-
// (okf/plans/component-gestures.md).
|
|
18
|
-
|
|
19
|
-
export type ArenaOwner = {
|
|
20
|
-
/** Retract the gesture without firing; invoked when the pointer is stolen. */
|
|
21
|
-
cancel(): void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type Claim = { owner: ArenaOwner; resolved: boolean }
|
|
25
|
-
|
|
26
|
-
let claims = new Map<number, Claim>()
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Provisionally claim an unowned pointer. Returns false (claim refused) when
|
|
30
|
-
* any recognizer already owns it. The claim is stealable until the owner
|
|
31
|
-
* releases it or a steal resolves the arena.
|
|
32
|
-
*/
|
|
33
|
-
export function claim(pointerId: number, owner: ArenaOwner): boolean {
|
|
34
|
-
if (claims.has(pointerId)) return false
|
|
35
|
-
claims.set(pointerId, { owner, resolved: false })
|
|
36
|
-
return true
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Take the pointer on positive evidence of a gesture, cancelling the current
|
|
41
|
-
* provisional owner (if any), and resolve the arena: the resulting claim
|
|
42
|
-
* cannot be stolen. Returns false when the arena is already resolved, in
|
|
43
|
-
* which case the caller lost and must stand down.
|
|
44
|
-
*/
|
|
45
|
-
export function steal(pointerId: number, owner: ArenaOwner): boolean {
|
|
46
|
-
let current = claims.get(pointerId)
|
|
47
|
-
if (current) {
|
|
48
|
-
if (current.resolved) return false
|
|
49
|
-
current.owner.cancel()
|
|
50
|
-
}
|
|
51
|
-
claims.set(pointerId, { owner, resolved: true })
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Release the claim on a pointer, if `owner` still holds it. */
|
|
56
|
-
export function release(pointerId: number, owner: ArenaOwner): void {
|
|
57
|
-
if (claims.get(pointerId)?.owner === owner) claims.delete(pointerId)
|
|
58
|
-
}
|
package/src/pan.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { onSettled } from "@solidrt/core"
|
|
2
|
-
import type { PointerEvent } from "@solidrt/core"
|
|
3
|
-
import { release, steal } from "./arena"
|
|
4
|
-
|
|
5
|
-
// Movement in logical pixels before a pan activates. Below this a drag still
|
|
6
|
-
// reads as a press (tap wiggle); crossing it is the positive evidence that the
|
|
7
|
-
// gesture is a pan, at which point the pan steals the pointer in the arena.
|
|
8
|
-
const PAN_SLOP = 8
|
|
9
|
-
|
|
10
|
-
export type PanAxis = "vertical" | "horizontal" | "both"
|
|
11
|
-
|
|
12
|
-
export interface PanOptions {
|
|
13
|
-
/**
|
|
14
|
-
* Which movement direction activates the pan. Slop is axis-aware: a
|
|
15
|
-
* "vertical" pan only activates once vertical travel crosses the threshold,
|
|
16
|
-
* so nested cross-axis scrollers each take only drags along their own axis.
|
|
17
|
-
* Default "both" (straight-line distance).
|
|
18
|
-
*/
|
|
19
|
-
axis?: PanAxis
|
|
20
|
-
onPanStart?: () => void
|
|
21
|
-
/** Pointer movement since the previous event; positive dx is rightward, positive dy downward. */
|
|
22
|
-
onPanMove?: (dx: number, dy: number) => void
|
|
23
|
-
onPanEnd?: () => void
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// The pan recognizer: turns a drag into a movement-delta stream. On a down it
|
|
27
|
-
// arms and starts measuring; when travel from the down point crosses the slop
|
|
28
|
-
// along the enabled axis it activates, stealing the pointer in the arena (a
|
|
29
|
-
// press that provisionally owned it is cancelled - its feedback retracts) and
|
|
30
|
-
// resolving it so no other recognizer can take the drag over. If the arena is
|
|
31
|
-
// already resolved (an inner pan won first) the recognizer disarms and stays
|
|
32
|
-
// out. The slop distance itself is swallowed: deltas stream from the
|
|
33
|
-
// activation point on. Moves and the up arrive on the frozen down path, so an
|
|
34
|
-
// active pan keeps streaming when the pointer leaves the node or the window.
|
|
35
|
-
// cancel() is the external-cancel hook; it ends an active pan without
|
|
36
|
-
// onPanEnd. Options are read at event time. Deliberately framework-agnostic:
|
|
37
|
-
// a candidate for promotion into core (okf/plans/component-gestures.md).
|
|
38
|
-
export function createPan(options: PanOptions) {
|
|
39
|
-
// Down position while armed; last delivered position while active.
|
|
40
|
-
let origin: { x: number; y: number } | null = null
|
|
41
|
-
let active: number | null = null
|
|
42
|
-
let armed: number | null = null
|
|
43
|
-
|
|
44
|
-
let past = (e: PointerEvent) => {
|
|
45
|
-
if (!origin) return false
|
|
46
|
-
let dx = Math.abs(e.clientX - origin.x)
|
|
47
|
-
let dy = Math.abs(e.clientY - origin.y)
|
|
48
|
-
let axis = options.axis ?? "both"
|
|
49
|
-
if (axis === "vertical") return dy >= PAN_SLOP
|
|
50
|
-
if (axis === "horizontal") return dx >= PAN_SLOP
|
|
51
|
-
return dx * dx + dy * dy >= PAN_SLOP * PAN_SLOP
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
let reset = () => {
|
|
55
|
-
if (active != null) {
|
|
56
|
-
release(active, owner)
|
|
57
|
-
active = null
|
|
58
|
-
}
|
|
59
|
-
armed = null
|
|
60
|
-
origin = null
|
|
61
|
-
}
|
|
62
|
-
let cancel = reset
|
|
63
|
-
let owner = { cancel }
|
|
64
|
-
|
|
65
|
-
// An unmount mid-drag must not leave a resolved claim behind.
|
|
66
|
-
onSettled(() => reset)
|
|
67
|
-
|
|
68
|
-
let handlers = {
|
|
69
|
-
onPointerDown: (e: PointerEvent) => {
|
|
70
|
-
if (e.button != null && e.button !== 0) return
|
|
71
|
-
if (armed == null && active == null) {
|
|
72
|
-
armed = e.pointerId
|
|
73
|
-
origin = { x: e.clientX, y: e.clientY }
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
onPointerMove: (e: PointerEvent) => {
|
|
77
|
-
if (armed === e.pointerId && past(e)) {
|
|
78
|
-
if (steal(e.pointerId, owner)) {
|
|
79
|
-
active = e.pointerId
|
|
80
|
-
armed = null
|
|
81
|
-
origin = { x: e.clientX, y: e.clientY }
|
|
82
|
-
options.onPanStart?.()
|
|
83
|
-
} else {
|
|
84
|
-
// The arena is resolved against us; the drag belongs elsewhere.
|
|
85
|
-
reset()
|
|
86
|
-
}
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
if (active === e.pointerId && origin) {
|
|
90
|
-
options.onPanMove?.(e.clientX - origin.x, e.clientY - origin.y)
|
|
91
|
-
origin = { x: e.clientX, y: e.clientY }
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
onPointerUp: (e: PointerEvent) => {
|
|
95
|
-
if (active === e.pointerId) {
|
|
96
|
-
reset()
|
|
97
|
-
options.onPanEnd?.()
|
|
98
|
-
} else if (armed === e.pointerId) {
|
|
99
|
-
reset()
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { handlers, cancel }
|
|
105
|
-
}
|