@jsenv/navi 0.29.417 → 0.29.419

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/dev/jsenv_navi.js +347 -83
  3. package/dist/dev/jsenv_navi.js.map +12 -12
  4. package/dist/jsenv_navi.js +345 -81
  5. package/dist/jsenv_navi.js.map +12 -12
  6. package/docs/AI_INSTRUCTIONS.md +177 -258
  7. package/docs/actions.md +164 -251
  8. package/docs/autofocus.md +66 -83
  9. package/docs/control_object.md +117 -214
  10. package/docs/control_value.md +121 -185
  11. package/docs/create_and_edit.md +159 -254
  12. package/docs/css_architecture.md +196 -260
  13. package/docs/data_states.md +41 -44
  14. package/docs/dialog_shape.md +91 -134
  15. package/docs/drag_interactions.md +291 -454
  16. package/docs/drag_to_travel.md +152 -748
  17. package/docs/dynamic_import.md +119 -136
  18. package/docs/error_handling.md +75 -127
  19. package/docs/field_validation.md +49 -89
  20. package/docs/form_changed.md +61 -78
  21. package/docs/group.md +141 -0
  22. package/docs/i18n.md +31 -76
  23. package/docs/interactions.md +170 -326
  24. package/docs/list_action.md +36 -51
  25. package/docs/list_refresh.md +73 -115
  26. package/docs/mobile_touch.md +56 -62
  27. package/docs/navigation.md +377 -723
  28. package/docs/network_policy.md +61 -75
  29. package/docs/pan_zoom.md +62 -103
  30. package/docs/popup_backdrop.md +128 -168
  31. package/docs/popup_lift.md +179 -268
  32. package/docs/popup_open.md +380 -651
  33. package/docs/resource.md +228 -408
  34. package/docs/route_transitions.md +333 -473
  35. package/docs/safe_area.md +102 -89
  36. package/docs/scroll.md +175 -317
  37. package/docs/state_binding.md +40 -84
  38. package/docs/testid.md +18 -23
  39. package/docs/typography.md +111 -141
  40. package/docs/view_transitions.md +106 -104
  41. package/docs/z_index.md +61 -109
  42. package/package.json +2 -2
  43. package/docs/badge_list.md +0 -83
  44. package/docs/control_group.md +0 -168
  45. package/docs/mobile_layout_pitfalls.md +0 -145
@@ -3,9 +3,9 @@
3
3
  One declaration says whether a request may leave, and the action layer reads it
4
4
  everywhere it matters. Two things follow from it:
5
5
 
6
- - **writes are held.** A control bound to a `POST`, `PUT`, `PATCH` or `DELETE`
7
- action — or sitting in a form bound to one — is read-only and says why,
8
- before the press.
6
+ - **writes are held.** A control holding a `POST`, `PUT`, `PATCH` or `DELETE`
7
+ action — or sitting in a form holding one — is read-only and says why, before
8
+ the press.
9
9
  - **reads are answered from the store.** A resource `GET` completes with the row
10
10
  it already holds and asks nothing.
11
11
 
@@ -17,36 +17,24 @@ are the entire point — wants only the first, and says `reads: "network"`.
17
17
  ## What navi keeps without being asked
18
18
 
19
19
  What an app wants when the network is gone is one sentence: **answer from what
20
- you already hold, ask nothing, and refuse writes politely.** Nothing in it is
21
- about one app; navi does it once, and an app only has to say _when_.
22
-
23
- Leaving a page erases nothing. Four facts of the action layer add up to a
24
- cache the app never has to write:
25
-
26
- - **The data of a resource action is the store row.** A `GET` completes with an
27
- id, and `action.data` is `store.select(id)`, live. Whatever was read once is
28
- still there, updated by every later response that mentions it.
29
- - **A route action left behind is aborted, not reset.** Its value survives; only
30
- a run in flight is called off.
31
- - **Running a completed action is a no-op.** Coming back to `/games/abc` from
32
- the list sends nothing while the instance that read it is alive — `run()` on a
33
- `COMPLETED` action already has its data. Only `rerun()` goes back to the
34
- network (see [actions.md](./actions.md)). An instance nothing references any
35
- more is asked again, with the store row drawn meanwhile; under the policy
36
- that ask is answered from the store (below).
37
- - **A failed rerun keeps the previous value.** Only `errorSignal` and the
38
- running state move; what was on screen stays on screen.
39
-
40
- So an app that caches responses in a `Map` beside navi is keeping a second copy
41
- of what the store holds — and going stale on its own.
20
+ you already hold, ask nothing, and refuse writes politely.** navi does it once,
21
+ and an app only has to say _when_. Leaving a page erases nothing: the data of a
22
+ resource action is the store row, live, updated by every later response that
23
+ mentions it; a route action left behind is aborted, not reset; coming back to
24
+ `/games/abc` sends nothing while the instance that read it is alive, since
25
+ `run()` on a completed action is a no-op
26
+ ([actions.md](./actions.md#running-run-rerun-prerun-reset)), and an instance
27
+ nothing references any more asks again with the store row drawn meanwhile; a
28
+ failed rerun keeps the previous value. So an app that caches responses in a
29
+ `Map` beside navi is keeping a second copy of what the store holds — and going
30
+ stale on its own.
42
31
 
43
32
  ## The one thing the app decides: the reason
44
33
 
45
- Which moments count as "offline" is the app's. A device without a network
34
+ Which moments count as "offline" is the app's: a device without a network
46
35
  interface, a mode chosen in the settings, an outage the user agreed to stop
47
- fighting — these are not said the same way to the user, and an app may have
48
- none, one or all of them. Navi asks for a single value, the **reason**, and
49
- reads it everywhere it matters:
36
+ fighting are not said the same way to the user. navi asks for a single value,
37
+ the **reason**, and reads it everywhere it matters:
50
38
 
51
39
  ```js
52
40
  import { setNetworkPolicy } from "@jsenv/navi";
@@ -61,7 +49,7 @@ setNetworkPolicy(offlineReasonSignal, {
61
49
  ```
62
50
 
63
51
  The source may be a signal (followed live), a function, or a plain value. Set
64
- it once, at startup; there is nothing else to wire.
52
+ it once, at startup.
65
53
 
66
54
  ## What the policy changes
67
55
 
@@ -71,28 +59,27 @@ was asked:
71
59
 
72
60
  - **A `GET` answers from the store.** If the row it designates is there, the
73
61
  action completes with it and nothing is asked. The row is the one its params
74
- name — by the resource's `idKey`, or by any of its `uniqueKeys` — or, when
75
- the params name none (a `GET` without params, like `/me`), the row the
76
- action last completed with. Going from game A to game B, both read
77
- before, is a change of params that would normally rerun `GAME.GET`; under the
78
- policy it completes from the store, and a screen that does not take
79
- `error: true` keeps drawing what it holds instead of falling into its error
80
- boundary. A route opening a user by id **or** by slug gives navi both keys to
81
- look by: `resource("user", { uniqueKeys: ["slug"], … })`.
62
+ name — by the resource's `idKey`, or by any of its `uniqueKeys` — or, when the
63
+ params name none (a `GET` without params, like `/me`), the row the action last
64
+ completed with. Going from game A to game B, both read before, is a change of
65
+ params that would normally rerun `GAME.GET`; under the policy it completes
66
+ from the store, and a screen that does not take `error: true` keeps drawing
67
+ what it holds instead of falling into its error boundary. A route opening a
68
+ user by id **or** by slug gives navi both keys to look by:
69
+ `resource("user", { uniqueKeys: ["slug"], … })`.
82
70
  - **A completed read stays completed.** `GET_MANY` (and every other read) has
83
71
  nowhere to answer from: the store holds items, not queries, and only the
84
72
  action's own value knows which ids answered `/users?scope=shareable`. So a
85
73
  rerun asked of a completed read under the policy is held — the action keeps
86
74
  its state, its value and its data, exactly as a `run()` on a completed action
87
- would. This is the piece an app cannot do on its own: by the time a callback
88
- runs, its action has already been reset.
75
+ would.
89
76
  - **A `GET_RANGE` revalidation fails quietly.** A list that comes back to the
90
77
  screen draws the composition it left and asks again for the window it draws;
91
78
  under the policy that ask fails, and a failed revalidation keeps the rows it
92
79
  had. Only a window never loaded shows the failure.
93
80
  - **Everything else settles with a `NetworkPolicyError`.** A read with nothing
94
- in the store, a relationship `GET` (it has no row of its own to answer with), a
95
- write that got through anyway: the action ends `FAILED` with an error whose
81
+ in the store, a relationship `GET` (it has no row of its own to answer with),
82
+ a write that got through anyway: the action ends `FAILED` with an error whose
96
83
  `reason` is the policy's value. `isNetworkPolicyError(error)` tells it apart
97
84
  from a request that left and never came back — that one is the app's `fetch`
98
85
  rejecting, and the app names it. The class says the request never left, and
@@ -101,30 +88,30 @@ was asked:
101
88
 
102
89
  It is an error only in the way it travels. Nothing treats it as a bug: navi
103
90
  never reports it as unhandled, and when one reaches `window` anyway — a run
104
- called by hand whose failure nobody catches, a render throw no boundary took
105
- — navi cancels the event, so neither the browser console nor the jsenv
106
- overlay says anything. The person reads the screen; nobody else has to hear
107
- about it.
108
-
109
- One line is left, in dev only. A boundary displaying it — `<ErrorBoundary>`,
110
- a control with `actionErrorEffect="throw"` — makes `preact/debug` log it with
111
- `console.error`, as it logs every error a boundary catches, 404s and failed
112
- fetches included. `preact/debug` reads no option or flag that would say the
113
- error is handled, so that line stays. The jsenv overlay stays quiet, and a
91
+ called by hand whose failure nobody catches, a render throw no boundary took —
92
+ navi cancels the event, so neither the browser console nor the jsenv overlay
93
+ says anything. One line is left, in dev only: a boundary displaying it
94
+ (`<ErrorBoundary>`, a control with `actionErrorEffect="throw"`) makes
95
+ `preact/debug` log it with `console.error`, as it logs every error a boundary
96
+ catches, and `preact/debug` reads nothing that would say it is handled. A
114
97
  build does not load `preact/debug`.
115
98
 
116
- - **A write is refused before the press.** A control bound to a `POST`, `PUT`,
117
- `PATCH` or `DELETE` action — or any control inside a `<Form>` bound to one —
118
- is read-only while the policy holds, and answers the press with
119
- `readOnlyMessage`. Nothing to add per button.
99
+ - **A write is refused before the press.** A control holding a write verb's
100
+ instance — `GAME.DELETE`, `GAME.DELETE.bindParams({ id })` — or any control
101
+ inside a `<Form>` holding one, is read-only while the policy holds, and
102
+ answers the press with `readOnlyMessage`.
120
103
 
121
104
  Which actions the policy sees: those declaring a verb (`meta.verb`) — every
122
- action a `resource()` makes. A plain `createAction` may not touch the network
123
- at all, so it is left alone. `createAction(callback, { meta: { verb } })` opts
124
- it into what the action layer holds — a control bound to a write verb is
125
- read-only, a completed `"GET"` is not rerun — but its callback is still called
126
- when it runs: answering from the store and settling with a `NetworkPolicyError`
127
- are done by the callbacks a `resource()` wraps.
105
+ action a `resource()` makes. A plain `createAction` may not touch the network at
106
+ all, so it is left alone, and so is a function handed to a control's `action`:
107
+ it is wrapped into an action with no verb, even when it calls one, so the
108
+ control is not held and the write fails after the press with a
109
+ `NetworkPolicyError` ([resource.md](./resource.md#a-function-calling-the-verb-or-the-instance)).
110
+ `createAction(callback, { meta: { verb } })` opts an action into what the action
111
+ layer holds — a control bound to a write verb is read-only, a completed `"GET"`
112
+ is not rerun — but its callback is still called when it runs: answering from
113
+ the store and settling with a `NetworkPolicyError` are done by the callbacks a
114
+ `resource()` wraps.
128
115
 
129
116
  ## Holding writes while reads go out
130
117
 
@@ -149,10 +136,9 @@ own account.
149
136
  What this buys over a guard in the app's own request layer is **when** the
150
137
  refusal happens. A guard one layer below sees a request that was already built:
151
138
  the press was accepted, the sheet opened, the form submitted, and only then does
152
- it fail. navi knows which controls are bound to a write before any of that, so
153
- the button says why instead of being pressed, and the dialog it would have
154
- opened never opens. A button that always fails teaches people not to press
155
- buttons.
139
+ it fail. navi knows which controls hold a write before any of that, so the
140
+ button says why instead of being pressed, and the dialog it would have opened
141
+ never opens. A button that always fails teaches people not to press buttons.
156
142
 
157
143
  ## Two reasons at once
158
144
 
@@ -174,8 +160,8 @@ setNetworkPolicy(networkPolicyReasonSignal, {
174
160
 
175
161
  `reads` is read from the reason for exactly this: one declaration, and the
176
162
  reason decides which kind of policy is holding. The `??` above is the
177
- precedence, and it is the app's to write — when both hold the same write, which
178
- one says why is something navi has nothing to decide it with.
163
+ precedence, and it is the app's to write: when both hold the same write, navi
164
+ has nothing to decide which one says why.
179
165
 
180
166
  ## What stays the app's
181
167
 
@@ -195,14 +181,14 @@ one says why is something navi has nothing to decide it with.
195
181
  - **What a screen says on a `NetworkPolicyError`** — "no network" is a fact
196
182
  about the device, "offline mode" a decision, "we are only looking" neither;
197
183
  `error.reason` is there so the screen says the right one.
198
- - **Which answer is worth keeping across a reload.** A resource declaring
184
+ - **Which answer is worth keeping across a reload** — which resource, into
185
+ which signal, for which session: a resource declaring
199
186
  [`persist`](./resource.md#persist-the-last-answer-drawn-again-after-a-reload)
200
- has its last `GET` answer back in the store at the first run, and under the
201
- policy that row answers the `GET`: a reload offline reopens on it. Which
202
- resource, into which signal, and for which session, is the app's to say.
203
- - **A queue of writes to replay** once the network is back. Deliberately not
204
- navi's: what a replayed write means (a score entered twice? a seat taken
205
- since?) is the app's business.
187
+ has its kept row answer the `GET` under the policy, so a reload offline
188
+ reopens on it.
189
+ - **A queue of writes to replay** once the network is back — not navi's: what a
190
+ replayed write means (a score entered twice? a seat taken since?) is the app's
191
+ business.
206
192
 
207
193
  ## See also
208
194
 
package/docs/pan_zoom.md CHANGED
@@ -36,20 +36,19 @@ them panning and the distance between them zooming, and a finger lifting
36
36
  re-anchors on what is left. What a pixel of pan means in your coordinates, and
37
37
  whether the zoom is continuous or stepped, is yours: the numbers are the numbers.
38
38
 
39
- What navi holds is what has to be settled **before** the press: `touch-action` on
40
- the surface, said from a stylesheet because a browser decides what a touch may do
41
- when it lands; the pan stepping back for what is carried across the surface (a
42
- `move`, a handle, a field, a popover — the same list a travelling box steps back
43
- for); the pinch not beginning as a pan under its first finger; the wheel and the
44
- pinch writing one `zoom`; the capture, the pointer the browser drops, the click
45
- the release leaves behind, and the next tap's click, which Chrome on Android
46
- drops after a touch drag whose `touchmove`s nobody refused (see
47
- [mobile_touch.md](./mobile_touch.md#a-tap-dropped-after-a-touch-drag)).
48
- A pointer pans only once it has travelled a few px (`data-drag-threshold`), so a
49
- tap stays a tap: a `longpress` declared beside `pan` still gets its hold, and a
50
- `double_click` still gets its two presses. A press that does pan is the
51
- surface's: a `longpress` waiting on it gives up the moment the pan begins, even
52
- inside its own slop.
39
+ Everything else is settled before the press, and there is nothing to wire: the
40
+ pan steps back for what is carried across the surface (a `move`, a handle, a
41
+ field, a popover — the same list a travelling box steps back for), the pinch does
42
+ not begin as a pan under its first finger, the wheel and the pinch write one
43
+ `zoom`, the click the release leaves behind is swallowed, and the next tap's is
44
+ kept — Chrome on Android drops it after a touch drag whose `touchmove`s nobody
45
+ refused (see
46
+ [mobile_touch.md](./mobile_touch.md#a-tap-dropped-after-a-touch-drag)). A pointer
47
+ pans only once it has travelled a few px (`data-drag-threshold`), so a tap stays
48
+ a tap: a `longpress` declared beside `pan` still gets its hold, and a
49
+ `double_click` its two presses. A press that does pan is the surface's: a
50
+ `longpress` waiting on it gives up the moment the pan begins, even inside its own
51
+ slop.
53
52
 
54
53
  Declared alone, `zoom` takes two fingers and the wheel and leaves one pointer to
55
54
  whatever else reads it: nothing captures it, waits on it or swallows its click
@@ -62,9 +61,8 @@ the page.
62
61
 
63
62
  A surface takes every touch that lands on it, which is right for a map filling
64
63
  the screen — there is no page left behind it to scroll. A plan shown as a
65
- thumbnail on a page is the other case: a finger there means to scroll nine times
66
- out of ten, and a surface answering all of them makes the page unreadable past
67
- it.
64
+ thumbnail on a page is the other case: a finger there usually means to scroll,
65
+ and a surface answering every one makes the page unreadable past it.
68
66
 
69
67
  ```jsx
70
68
  <Box data-pan-after-hold interactions={{ pan, zoom }} />
@@ -74,20 +72,16 @@ The finger then says it means THIS surface the way it says it means to carry a
74
72
  drag source — by standing still. Until the hold the page keeps its scroll, and
75
73
  the pan starts where the finger already is. The pinch is not given away with it:
76
74
  two fingers on a surface that answers `zoom` are its own. A mouse travelling is
77
- untouched — a button held down over a surface could never have meant a scroll,
78
- and its wheel is settled on its own (see below) — and it is read off the element
79
- or any ancestor, the same place `data-drag-on-contact` is said.
75
+ untouched — a button held down over a surface could never have meant a scroll.
76
+ It is read off the element or any ancestor, like `data-drag-on-contact`.
80
77
 
81
- Opt-in, and for the same reason its opposite is (see
82
- [`data-drag-on-contact`](./drag_interactions.md#a-finger-that-does-not-have-to-wait-data-drag-on-contact)):
83
- navi cannot see whether anything behind the surface scrolls. You know;
84
- say so. It spends the hold, though: a `longpress` declared beside a `pan` that
85
- waits asks one finger to answer two waits, and the surface's — the shorter one
86
- — is the one answered.
87
-
88
- On a surface that only zooms there is no pan to wait for: one finger is the
89
- page's to scroll, two are the surface's, and `"kept"` (below) has nothing to
90
- keep.
78
+ Opt-in, for the same reason its opposite
79
+ [`data-drag-on-contact`](./drag_interactions.md#a-finger-that-does-not-have-to-wait-data-drag-on-contact)
80
+ is: navi cannot see whether anything behind the surface scrolls. You know; say
81
+ so. It spends the hold: a `longpress` declared beside a `pan` that waits asks one
82
+ finger to answer two waits, and the surface's — the shorter one — is the one
83
+ answered. On a surface that only zooms there is no pan to wait for: one finger is
84
+ the page's, two are the surface's, and `"kept"` (below) has nothing to keep.
91
85
 
92
86
  The wait is then asked before every pan, which is right while each finger
93
87
  landing there is genuinely ambiguous. Once the user has settled into the plan it
@@ -98,14 +92,11 @@ is not, and asking again is asking three times for one sentence:
98
92
  ```
99
93
 
100
94
  From the moment the surface is given the hand — the hold, a pinch, a mouse
101
- travelling — it pans on contact, the way the same plan does opened full screen.
102
- It asks again only after a press has landed away from it, which is the hand
103
- saying it has moved on; navi watches for that press itself, so nothing on your
104
- side listens to the window.
105
-
106
- The paid wait is a state to draw — the map answers on contact now, and the user
107
- has to be told — so the surface says it: `[data-hand-kept]`, for as long as it
108
- holds the hand, a finger on it or not.
95
+ travelling — it pans on contact, the way the same plan does opened full screen,
96
+ until a press lands away from it; navi watches for that press itself, so nothing
97
+ on your side listens to the window. The paid wait is a state to draw, and only a
98
+ `"kept"` surface carries it: `[data-hand-kept]`, for as long as it holds the
99
+ hand, a finger on it or not.
109
100
 
110
101
  ```css
111
102
  .plan[data-hand-kept] {
@@ -113,23 +104,20 @@ holds the hand, a finger on it or not.
113
104
  }
114
105
  ```
115
106
 
116
- Only a `"kept"` surface ever carries it: one that answers on contact was never
117
- given anything to keep. `[data-grabbed]` is the narrower word — a hand on it at
118
- this instant — and the two are drawn together or apart as you like
119
- (`[data-hand-kept]:not([data-grabbed])` is the surface waiting, held, for the
120
- next finger).
107
+ `[data-grabbed]` is the narrower word — a hand on it at this instant — so
108
+ `[data-hand-kept]:not([data-grabbed])` is the surface waiting, held, for the
109
+ next finger.
121
110
 
122
111
  ### A wheel that does not steal the page's scroll
123
112
 
124
- A wheel over a surface in a page means to scroll that page nine times out of ten
125
- — it is what a wheel means everywhere else on it, and a surface that answers all
126
- of them makes the page unreadable past it, the same way a finger taking every
127
- touch does.
128
-
129
- So a bare wheel zooms only where nothing around the surface would have scrolled:
130
- a map filling the screen, a board in a modal. Where something would — a plan
131
- shown in the middle of a page — the page keeps its scroll and the zoom is one
132
- key away:
113
+ A wheel over a surface in a page means to scroll that page just as often as a
114
+ finger does, and is settled the other way for one reason: what a TOUCH may do is
115
+ decided before it lands, from `touch-action`, so only the caller can say it; a
116
+ WHEEL is read, and by the time the event is there, what scrolls around the
117
+ surface can simply be looked up. So nothing is declared: a bare wheel zooms only
118
+ where nothing around the surface would have scrolled — a map filling the screen,
119
+ a board in a modal (the walk stops at a modal) — and where something would, the
120
+ page keeps its scroll and the zoom is one key away:
133
121
 
134
122
  | wheel over the surface | in a page that scrolls | where nothing scrolls |
135
123
  | ---------------------- | ---------------------- | --------------------- |
@@ -137,22 +125,12 @@ key away:
137
125
  | `ctrl` / `meta` held | zooms | zooms |
138
126
  | trackpad pinch | zooms | zooms |
139
127
 
140
- Nothing is declared for this, unlike `data-pan-after-hold` right above — and the
141
- difference is worth knowing, because it is the same question asked twice. What a
142
- TOUCH may do is settled before it lands, from `touch-action`, so nothing can be
143
- read at that point and only the caller knows. A WHEEL is read: by the time the
144
- event is there, what scrolls around the surface can simply be looked up, so navi
145
- looks it up instead of asking. The walk stops at a modal, whose page behind is
146
- not what a wheel over it is for.
147
-
148
128
  A refused wheel is not a wheel that did nothing: navi says what it is waiting for
149
129
  (`⌘ + scroll to zoom`, `Ctrl` off a Mac) in a callout over the surface, which
150
- goes away with the gesture. It is navi's own text, `interaction.zoom.needs_modifier`
151
- — override it through `naviI18n` like any other (see `docs/i18n.md`).
152
-
153
- The trackpad pinch arrives as a wheel with `ctrl` held, which is why it goes on
154
- zooming everywhere: it is the desk's version of two fingers on a phone, and it
155
- was never the page's.
130
+ goes away with the gesture — navi's own text, `interaction.zoom.needs_modifier`,
131
+ overridden through `naviI18n` like any other (see [i18n.md](./i18n.md)). The
132
+ trackpad pinch arrives as a wheel with `ctrl` held, which is why it zooms
133
+ everywhere: it is the desk's version of two fingers on a phone.
156
134
 
157
135
  ```jsx
158
136
  <Box data-zoom-on-contact interactions={{ pan, zoom }} />
@@ -160,32 +138,16 @@ was never the page's.
160
138
 
161
139
  `data-zoom-on-contact` takes the bare wheel back, for a surface that owns it
162
140
  whatever stands around it — a map whose page happens to scroll a little, an
163
- editor where the wheel is a tool. Read off the element or any ancestor, the same
164
- place `data-pan-after-hold` is.
141
+ editor where the wheel is a tool. Read off the element or any ancestor.
165
142
 
166
143
  ### When the surface has the hand: `grab`, `release`, `[data-grabbed]`
167
144
 
168
145
  A surface that is asked for — by a hold, by the first few pixels of travel, or
169
146
  by a second finger — has an instant where it becomes the hand's, and nothing on
170
- screen says it. The hand then moves too early and scrolls the page instead, or
171
- waits long past the moment out of doubt. So the surface says it, the same two
172
- words a carried element says (`grab`, `release`) and the same attribute:
173
-
174
- ```jsx
175
- <Box
176
- data-pan-after-hold
177
- interactions={{
178
- pan,
179
- zoom,
180
- grab: (event) => {
181
- if (event.detail.pointerType === "touch") {
182
- navigator.vibrate?.(10);
183
- }
184
- },
185
- release: () => {},
186
- }}
187
- />
188
- ```
147
+ screen says it: the hand moves too early and scrolls the page, or waits long past
148
+ the moment out of doubt. So the surface says it with the two words and the
149
+ attribute a carried element uses (see
150
+ [drag_interactions.md](./drag_interactions.md#saying-the-grab-is-acquired-grab)).
189
151
 
190
152
  ```css
191
153
  .plan[data-grabbed] {
@@ -195,20 +157,17 @@ words a carried element says (`grab`, `release`) and the same attribute:
195
157
 
196
158
  `[data-grabbed]` is on the element for as long as a hand is on the surface (on
197
159
  one that only zooms, from the second finger landing to the last one lifting), so
198
- a contour, a veil or a raised shadow needs no listener — which is what this is
199
- usually for. (`[data-hand-kept]` is the other one, and it outlasts the finger:
200
- see [`data-pan-after-hold`](#a-surface-that-scrolls-past-data-pan-after-hold).) `grab` and `release` are for the rest: a vibration, a state kept
201
- elsewhere. Their detail is `{ pointerType }`; like a drag's, they **report and do
202
- not ask**, and declared without `pan` or `zoom` they are a drag's words again,
203
- which is what the dev warning says.
204
-
205
- **Do not read the capture instead.** `setPointerCapture` is what the gesture takes
206
- at that same instant, and it is the wrong signal twice over: the browser does not
207
- announce a capture when it is taken but just before the NEXT pointer event for
208
- that pointer, so a finger that holds still and then keeps still is announced
209
- nothing at all, and a finger that moves is told at the very moment the surface is
210
- already moving under it. With a mouse it lands mid-travel. `grab` is told where it
211
- happens, before the first `pan`.
160
+ a contour, a veil or a raised shadow needs no listener. `grab` and `release`,
161
+ declared beside `pan`/`zoom`, are for the rest — a vibration on the touch that
162
+ took the surface, a state kept elsewhere. Their detail is `{ pointerType }`; they
163
+ report and do not ask, and declared without `pan` or `zoom` they are a drag's
164
+ words again, which the dev warning says.
165
+
166
+ **Do not read the pointer capture instead.** The browser announces it just
167
+ before the NEXT pointer event, so a finger held still is announced nothing and a
168
+ finger that moves is told when the surface is already moving under it (see
169
+ [mobile_touch.md](./mobile_touch.md#a-finger-is-captured-to-what-it-touched-inside-shadow-trees-too)).
170
+ `grab` is told where it happens, before the first `pan`.
212
171
 
213
172
  ## Reference
214
173
 
@@ -217,5 +176,5 @@ happens, before the first `pan`.
217
176
  page, on `installPanZoom` from `@jsenv/dom`.
218
177
  - `@jsenv/dom` — `src/interaction/drag/pan_zoom.js`: the pointers, the pinch,
219
178
  the wheel and what it is given to.
220
- - `src/control/demos/38_interactions_demo.html` — a surface panned and zoomed.
221
- - `src/control/demos/integration/5_plan_editor_demo.html` — a plan editor.
179
+ - `src/control/demos/38_interactions_demo.html` — a surface panned and zoomed,
180
+ after a hold, and kept.