@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 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 offset+clamp
50
- geometry) and the internal `createPan`/arena (arena.ts, pan.ts, press.ts:
51
- per-pointer claims, innermost press wins, pan steals on slop).
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.46",
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.46"
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
- // Deliberately framework-agnostic (no theme, no styling): a candidate for
50
- // promotion into core once the recognizer family grows
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)
@@ -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
- }