@jsenv/navi 0.29.148 → 0.29.149
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/jsenv_navi.js +467 -269
- package/dist/jsenv_navi.js.map +27 -19
- package/docs/AI_INSTRUCTIONS.md +8 -6
- package/docs/actions.md +39 -5
- package/docs/error_handling.md +5 -0
- package/docs/interactions.md +83 -38
- package/docs/resource.md +19 -0
- package/package.json +3 -2
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -263,12 +263,14 @@ consistency across the app, not from any single call site.
|
|
|
263
263
|
of navi's own.
|
|
264
264
|
- `docs/interactions.md` — the `interactions` prop: making a component answer a
|
|
265
265
|
swipe, a held press, a shortcut, and registering a gesture navi does not have.
|
|
266
|
-
It also holds `
|
|
267
|
-
zone that belongs to another control — a chip's cross, an eye, a
|
|
268
|
-
the
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
`
|
|
266
|
+
It also holds `selfInteractions`, for an affordance an application draws
|
|
267
|
+
inside a zone that belongs to another control — a chip's cross, an eye, a
|
|
268
|
+
diskette: why the claim is a required LIST (a press is not a drag, and what
|
|
269
|
+
is not named stays the zone's), `whenSelfInteractionsBlocked` and the question
|
|
270
|
+
that picks one of its three modes (does it write to the control it sits in?),
|
|
271
|
+
the `Box` form that claims interactions and nothing more (so an affordance
|
|
272
|
+
keeps its own drawing instead of becoming a control), and what the claim does
|
|
273
|
+
NOT stop (a plain `onClick` on an ancestor).
|
|
272
274
|
Read it before reading the pointer by hand — who owns a press between nested
|
|
273
275
|
boxes, and what a touch may do, are decided before the first pixel moves and
|
|
274
276
|
cannot be got right from outside navi — and before stopping the propagation of
|
package/docs/actions.md
CHANGED
|
@@ -96,7 +96,9 @@ available while a rerun is in flight). Where a failure goes when no screen takes
|
|
|
96
96
|
it — and why a run settles with its error rather than rejecting — is
|
|
97
97
|
[error_handling.md](./error_handling.md). `useActionStatus(action)` gives the whole
|
|
98
98
|
state at once — `{ idle, loading, completed, aborted, error, data, params }` —
|
|
99
|
-
for a component that needs to look at it rather than render it.
|
|
99
|
+
for a component that needs to look at it rather than render it. It reads that
|
|
100
|
+
very instance: to know what moves when a control is handed the action, see
|
|
101
|
+
[the instance a control runs](#the-instance-a-control-runs).
|
|
100
102
|
|
|
101
103
|
**Reading does not run.** `useAsyncData` waits for data someone else asked for
|
|
102
104
|
— for a page, the route, through `routeAction`. A component reading an action
|
|
@@ -187,6 +189,38 @@ callback:
|
|
|
187
189
|
</Button>
|
|
188
190
|
```
|
|
189
191
|
|
|
192
|
+
## The instance a control runs
|
|
193
|
+
|
|
194
|
+
A control binds the action it is given to its own UI state and runs the result.
|
|
195
|
+
Which instance that is depends on whether the control carries a value:
|
|
196
|
+
|
|
197
|
+
- a control with **no value of its own** — a button — contributes no params, so
|
|
198
|
+
what it runs **is** the instance it was handed. `useActionStatus` on that
|
|
199
|
+
instance sees the click, the run, the data.
|
|
200
|
+
- a control that **carries a value** — an input, a form — runs the child
|
|
201
|
+
instance bound to that value. Those params are not the caller's, so neither
|
|
202
|
+
is the status: the instance the caller holds stays idle while the control's
|
|
203
|
+
run moves. Read the effect where it lands (the store, for a resource verb),
|
|
204
|
+
or listen to the run itself, below.
|
|
205
|
+
|
|
206
|
+
Every control with an `action` reports the run it performs, whichever instance
|
|
207
|
+
that is:
|
|
208
|
+
|
|
209
|
+
```jsx
|
|
210
|
+
<Button
|
|
211
|
+
action={shareAction}
|
|
212
|
+
onActionStart={(e) => {}}
|
|
213
|
+
onActionEnd={(data, e) => {}}
|
|
214
|
+
onActionError={(error, e) => {}}
|
|
215
|
+
onActionAborted={(e) => {}}
|
|
216
|
+
/>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`onActionEnd` receives the data of a run that **completed** — a failure never
|
|
220
|
+
reaches it. A failure goes to `onActionError`, alongside wherever the error is
|
|
221
|
+
displayed (see [error_handling.md](./error_handling.md)); an abort to
|
|
222
|
+
`onActionAborted`.
|
|
223
|
+
|
|
190
224
|
## `action` or `uiAction`
|
|
191
225
|
|
|
192
226
|
Both fire when a control's value changes, and they are not two ways of writing
|
|
@@ -257,10 +291,10 @@ once per row. See
|
|
|
257
291
|
[popup_open.md](./popup_open.md#a-press-that-opens-a-popup-and-acts-on-it).
|
|
258
292
|
|
|
259
293
|
What is left for an `onClick` is what no value can express — imperative work
|
|
260
|
-
with nothing to open and nothing to send. And one nuance worth knowing: on
|
|
261
|
-
`
|
|
262
|
-
gate rather than firing from the DOM (see
|
|
263
|
-
[interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-
|
|
294
|
+
with nothing to open and nothing to send. And one nuance worth knowing: on a
|
|
295
|
+
control with `selfInteractions`, a caller's `onClick` runs inside that control's
|
|
296
|
+
own interaction gate rather than firing from the DOM (see
|
|
297
|
+
[interactions.md](./interactions.md#an-affordance-inside-somebody-elses-box-selfinteractions)),
|
|
264
298
|
so the usual objection — an `onClick` fires on a read-only control — does not
|
|
265
299
|
apply there. Everywhere else it does.
|
|
266
300
|
|
package/docs/error_handling.md
CHANGED
|
@@ -41,6 +41,11 @@ validation message on the **requester** (the submit button, not the form):
|
|
|
41
41
|
else — `"none"` — means the control displays nothing because something else
|
|
42
42
|
does. `<Details>` uses that: its `<ActionRenderer>` shows the error inside the
|
|
43
43
|
open panel.
|
|
44
|
+
- `onActionError(error, event)` fires alongside whichever display was chosen —
|
|
45
|
+
it is for reacting to the failure, not for showing it (showing stays claimed
|
|
46
|
+
by the message or the boundary). Its siblings on every control with an
|
|
47
|
+
`action` — `onActionStart`, `onActionEnd`, `onActionAborted` — are in
|
|
48
|
+
[actions.md](./actions.md#the-instance-a-control-runs).
|
|
44
49
|
|
|
45
50
|
### A screen's data failed — a route action
|
|
46
51
|
|
package/docs/interactions.md
CHANGED
|
@@ -531,64 +531,94 @@ sideways), while a `move` goes wherever it is put, a `land` wherever the board h
|
|
|
531
531
|
places and a `toss` wherever it was thrown (`xy`). `data-drag-delay`,
|
|
532
532
|
`data-drag-slop`, `data-drag-threshold` tune when the press becomes a grab.
|
|
533
533
|
|
|
534
|
-
### An affordance inside somebody else's box: `
|
|
534
|
+
### An affordance inside somebody else's box: `selfInteractions`
|
|
535
535
|
|
|
536
536
|
A chip's cross inside a carried piece, an eye on a row that travels, a diskette
|
|
537
|
-
on a picker's façade. It is aimed AT, not merely inside
|
|
538
|
-
|
|
537
|
+
on a picker's façade. It is aimed AT, not merely inside — but aimed at for
|
|
538
|
+
WHAT, which is the whole of the prop:
|
|
539
539
|
|
|
540
540
|
```jsx
|
|
541
|
-
<Badge.Button
|
|
541
|
+
<Badge.Button selfInteractions="click" onClick={() => remove(id)}>
|
|
542
542
|
×
|
|
543
543
|
</Badge.Button>
|
|
544
544
|
```
|
|
545
545
|
|
|
546
|
-
|
|
547
|
-
the click, and its `onClick` waits for its own interaction gate
|
|
548
|
-
firing from the DOM.
|
|
546
|
+
The press is now this cross's alone: no navi control above it answers the
|
|
547
|
+
mousedown or the click, and its `onClick` waits for its own interaction gate
|
|
548
|
+
instead of firing from the DOM.
|
|
549
549
|
|
|
550
|
-
####
|
|
550
|
+
#### Why it is a list, and why it is required
|
|
551
551
|
|
|
552
|
-
|
|
552
|
+
A press is not a drag. A drag announces itself — a few pixels of travel with a
|
|
553
|
+
mouse, a long hold with a finger — and a click is the absence of both, so the
|
|
554
|
+
two can be told apart without anyone guessing at pointerdown. An affordance
|
|
555
|
+
that claimed every gesture at once would be a HOLE in whatever it sits in: a
|
|
556
|
+
badge drawn against the edge of a card is a seventh of that card, and precisely
|
|
557
|
+
the edge one grabs to carry it.
|
|
553
558
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
|
557
|
-
|
|
|
558
|
-
|
|
|
559
|
+
So the claim names its interactions, and what it does not name stays the zone's:
|
|
560
|
+
|
|
561
|
+
| written | takes | leaves |
|
|
562
|
+
| ------------------------------ | ------------------------- | ----------------------------------- |
|
|
563
|
+
| `selfInteractions="click"` | the press | the grab — the card is still carried by it |
|
|
564
|
+
| `selfInteractions="click drag"`| both | — |
|
|
565
|
+
| `selfInteractions="*"` | every gesture, now and later | — |
|
|
566
|
+
|
|
567
|
+
`"*"` is there for the case where it is true, not as a shorthand: it is the one
|
|
568
|
+
value that will silently swallow a gesture navi has not shipped yet.
|
|
569
|
+
|
|
570
|
+
`data-drag-ignore` says a different thing, to the gesture alone and for all of
|
|
571
|
+
them at once: the press there is none of the gesture's business, and the element
|
|
572
|
+
keeps both its cursor and its text selection.
|
|
573
|
+
|
|
574
|
+
#### Where the zone blocks: does it write to the control it sits in?
|
|
575
|
+
|
|
576
|
+
That question, and nothing else, picks `whenSelfInteractionsBlocked` — what
|
|
577
|
+
becomes of the affordance where the zone around it is disabled or read-only.
|
|
578
|
+
The claimed interactions are its subject, and only them: the ones left to the
|
|
579
|
+
zone were never this element's to block.
|
|
580
|
+
|
|
581
|
+
| what it does | written | on a blocked zone |
|
|
582
|
+
| ------------------------------------------------------------ | -------------------------------------------- | ----------------------------------------- |
|
|
583
|
+
| writes to it (a cross that removes, a stepper) | nothing — `"hide"` is the default | it goes |
|
|
584
|
+
| writes to it, and its presence says there is something there | `whenSelfInteractionsBlocked="refuse"` | it stays and refuses with a callout |
|
|
585
|
+
| never touches it (a diskette saving into MY address book) | `whenSelfInteractionsBlocked="ignore"` | nothing changes: still lit, still pressed |
|
|
559
586
|
|
|
560
587
|
A greyed cross that still removes is worse than no cross — hence the default.
|
|
561
|
-
`"
|
|
588
|
+
`"ignore"` is the other extreme and the caller owns it: the zone's read-only is
|
|
562
589
|
about a value the affordance does not write, so answering "read-only" to a
|
|
563
590
|
gesture that was never going to write anything says nothing true. Use it only
|
|
564
591
|
when that is really the case.
|
|
565
592
|
|
|
593
|
+
Busy is not on the list because busy does not block: it is the read-only a
|
|
594
|
+
running action sets on its way that does.
|
|
595
|
+
|
|
566
596
|
#### On something you draw yourself
|
|
567
597
|
|
|
568
|
-
`
|
|
569
|
-
control to claim its
|
|
570
|
-
class stays exactly what it was drawn as:
|
|
598
|
+
`selfInteractions` is a `Box` prop too, so an affordance does not have to become
|
|
599
|
+
a control to claim its interactions — a pastille positioned in a card's corner
|
|
600
|
+
by its own class stays exactly what it was drawn as:
|
|
571
601
|
|
|
572
602
|
```jsx
|
|
573
|
-
<Box as="button"
|
|
603
|
+
<Box as="button" selfInteractions="click" className="court_side" onClick={explain}>
|
|
574
604
|
```
|
|
575
605
|
|
|
576
|
-
On a box the prop does exactly one thing: it writes `data-
|
|
577
|
-
attribute is the claim — it is what the controls above read, and what the
|
|
606
|
+
On a box the prop does exactly one thing: it writes `data-self-interactions`.
|
|
607
|
+
That attribute is the claim — it is what the controls above read, and what the
|
|
578
608
|
gesture readers read (`data-drag-handle`, `data-drag-ignore` and friends are the
|
|
579
|
-
same vocabulary). Writing it by hand on an
|
|
580
|
-
and is the last resort: a typo there is
|
|
581
|
-
once.
|
|
609
|
+
same vocabulary), each picking its own word out of it. Writing it by hand on an
|
|
610
|
+
element navi does not render works and is the last resort: a typo there is
|
|
611
|
+
silent, whereas the prop is spelled once and checked.
|
|
582
612
|
|
|
583
|
-
|
|
584
|
-
a gate, a callout and a control's own read-only, none of which a box
|
|
585
|
-
claims
|
|
586
|
-
does about a held zone matters.
|
|
613
|
+
`whenSelfInteractionsBlocked` is the other half, and it belongs to controls: it
|
|
614
|
+
is about a gate, a callout and a control's own read-only, none of which a box
|
|
615
|
+
has. A box claims interactions and nothing more; put the affordance on a control
|
|
616
|
+
when what it does about a held zone matters.
|
|
587
617
|
|
|
588
618
|
#### When the affordance should sit OUTSIDE instead
|
|
589
619
|
|
|
590
|
-
`
|
|
591
|
-
decides is whether the affordance stays where the finger left it.
|
|
620
|
+
`selfInteractions` says a façade CAN yield a zone; it does not say it should.
|
|
621
|
+
What decides is whether the affordance stays where the finger left it.
|
|
592
622
|
|
|
593
623
|
An icon that lives inside its control while that control is showing, and
|
|
594
624
|
becomes a pill of its own once it is not, is a switch that moves when you flip
|
|
@@ -598,21 +628,36 @@ which is the whole gesture on a phone. `<ControlSwap>` is that row: two
|
|
|
598
628
|
controls taking turns in the middle, a fixed cap at each end.
|
|
599
629
|
|
|
600
630
|
So: an affordance that acts on what it sits in (a chip's cross, a stepper, an
|
|
601
|
-
eye on a row) belongs inside, and takes its press back with `
|
|
602
|
-
that swaps what is being shown belongs outside it, where it can stay put.
|
|
631
|
+
eye on a row) belongs inside, and takes its press back with `selfInteractions`.
|
|
632
|
+
One that swaps what is being shown belongs outside it, where it can stay put.
|
|
633
|
+
|
|
634
|
+
Whichever side of the frame it ends up on, the control must be told, because a
|
|
635
|
+
control draws affordances of its own and will otherwise draw a second one:
|
|
636
|
+
|
|
637
|
+
- **inside** — a field has slots for it, `Input.UI.LeftSlot`,
|
|
638
|
+
`Input.UI.RightSlot`, `Input.UI.IconSlot` (an icon sized on the line rather
|
|
639
|
+
than on a character) and `Input.UI.UnitSlot`. They label the field, so a
|
|
640
|
+
press lands on it rather than blurring it — except when the field is not
|
|
641
|
+
focused yet, where the slot may take the focus itself, which is what a clear
|
|
642
|
+
cross or a reveal-password eye needs.
|
|
643
|
+
- **outside** — the icon the type would have drawn has to go, or it sits two
|
|
644
|
+
centimetres from yours: `icon={null}`. It is the same prop that replaces it
|
|
645
|
+
(`icon={<MySvg />}`) and that leaves it alone (left out). A `search` field
|
|
646
|
+
still swaps that slot for its clear cross once it holds a value, whatever
|
|
647
|
+
`icon` says — the cross is about the value, not about the decoration.
|
|
648
|
+
|
|
649
|
+
```jsx
|
|
650
|
+
<Input type="search" icon={null} /> // the row draws the magnifier
|
|
651
|
+
```
|
|
603
652
|
|
|
604
653
|
#### navi steps back; a plain `onClick` does not
|
|
605
654
|
|
|
606
|
-
What
|
|
607
|
-
|
|
655
|
+
What the claim stops is navi answering: the controls above it, and the gestures
|
|
656
|
+
it named. It does **not** stop the event — the propagation is left whole, so
|
|
608
657
|
that everything which is not a navi interaction still sees the press it always
|
|
609
658
|
saw. A raw `onClick` on an ancestor is one of those, and still fires; stop it
|
|
610
659
|
there yourself if it must not.
|
|
611
660
|
|
|
612
|
-
`data-drag-ignore` says a narrower thing, to the gesture alone: the press there
|
|
613
|
-
is none of the gesture's business, and the element keeps both its cursor and its
|
|
614
|
-
text selection.
|
|
615
|
-
|
|
616
661
|
### What says a thing can be picked up
|
|
617
662
|
|
|
618
663
|
Almost nothing, on purpose. A **handle** (`data-drag-handle`) exists only to drag,
|
package/docs/resource.md
CHANGED
|
@@ -138,6 +138,13 @@ One verb dispatching on a string gives up everything the store does for you:
|
|
|
138
138
|
per-operation action state (loading/error per button), per-relation autorerun,
|
|
139
139
|
and a child collection that other components can read.
|
|
140
140
|
|
|
141
|
+
The same smell wears a second coat: a verb on one relation carrying, in its
|
|
142
|
+
params, an id from a **different** relation — a `group_id` sent to the game ×
|
|
143
|
+
user shares, which the server expands into people. That is two relations
|
|
144
|
+
spelled as one, and the one the gesture is actually about (game × group) is the
|
|
145
|
+
one that ends up with no rows, no action state, and nothing for its row on
|
|
146
|
+
screen to read.
|
|
147
|
+
|
|
141
148
|
| Situation | Use |
|
|
142
149
|
| ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
150
|
| The child is a first-class entity with its own store, shared across parents (a user referenced by many games) | `.one()` / `.many()` |
|
|
@@ -149,6 +156,18 @@ and a child collection that other components can read.
|
|
|
149
156
|
Singular vs plural is about the relation, not the verb: `.one`/`.scopedOne` for a
|
|
150
157
|
single sub-object, `.many`/`.scopedMany` for a collection.
|
|
151
158
|
|
|
159
|
+
And the table has a negative worth naming, because the workaround looks fine in
|
|
160
|
+
a demo: **a write whose effect the response does not carry is a modelling bug,
|
|
161
|
+
not a case for the client to remember.** After the write, some row on screen
|
|
162
|
+
will claim "this happened" — and it must read that from the store, which only
|
|
163
|
+
holds what responses carried. When the relation actually written has no row
|
|
164
|
+
anywhere (the response answered with a different entity than the one the
|
|
165
|
+
gesture was about), whatever the screen memorizes instead lives on the client
|
|
166
|
+
and **dies with the session**: reload, and the row confidently claims the write
|
|
167
|
+
never happened. The fix is on the shape — give that relation its own
|
|
168
|
+
relationship method and a callback returning what was written, even when the
|
|
169
|
+
backend must grow a field for it — never a flag next to the button.
|
|
170
|
+
|
|
152
171
|
## Callback return contracts
|
|
153
172
|
|
|
154
173
|
These are not guessable — each relationship method has its own shape.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/navi",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.149",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Library of components including navigation to create frontend applications",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"prepublishOnly": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@jsenv/dom": "0.17.
|
|
32
|
+
"@jsenv/dom": "0.17.36",
|
|
33
33
|
"@jsenv/humanize": "1.7.8",
|
|
34
34
|
"@jsenv/validity": "0.4.3"
|
|
35
35
|
},
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@jsenv/plugin-preact": "../../related/plugin-preact",
|
|
41
41
|
"@jsenv/snapshot": "../../tooling/snapshot",
|
|
42
42
|
"@jsenv/terminal-table": "../../tooling/terminal-table",
|
|
43
|
+
"@jsenv/test": "../../related/test",
|
|
43
44
|
"@jsenv/urls": "../../tooling/urls",
|
|
44
45
|
"@preact/signals": "2.11.1",
|
|
45
46
|
"playwright": "1.62.1",
|