@jsenv/navi 0.29.388 → 0.29.390

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/docs/pan_zoom.md CHANGED
@@ -42,12 +42,21 @@ when it lands; the pan stepping back for what is carried across the surface (a
42
42
  `move`, a handle, a field, a popover — the same list a travelling box steps back
43
43
  for); the pinch not beginning as a pan under its first finger; the wheel and the
44
44
  pinch writing one `zoom`; the capture, the pointer the browser drops, the click
45
- the release leaves behind. A pointer pans only once it has travelled a few px
46
- (`data-drag-threshold`), so a tap stays a tap: a `longpress` declared beside
47
- `pan` still gets its hold, and a `double_click` still gets its two presses.
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_tap_suppression_after_drag.md](./mobile_tap_suppression_after_drag.md)).
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.
48
53
 
49
54
  Declared alone, `zoom` takes two fingers and the wheel and leaves one pointer to
50
- whatever else reads it; `pan` alone leaves the wheel to the page.
55
+ whatever else reads it: nothing captures it, waits on it or swallows its click
56
+ until a second finger lands, so a `click` or a `longpress` declared beside `zoom`
57
+ answers it as it would anywhere. From the second finger on, both are the pinch's,
58
+ and a `longpress` waiting on the first gives up. `pan` alone leaves the wheel to
59
+ the page.
51
60
 
52
61
  ### A surface that scrolls past: `data-pan-after-hold`
53
62
 
@@ -76,6 +85,10 @@ say so. It spends the hold, though: a `longpress` declared beside a `pan` that
76
85
  waits asks one finger to answer two waits, and the surface's — the shorter one
77
86
  — is the one answered.
78
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.
91
+
79
92
  The wait is then asked before every pan, which is right while each finger
80
93
  landing there is genuinely ambiguous. Once the user has settled into the plan it
81
94
  is not, and asking again is asking three times for one sentence:
@@ -152,11 +165,11 @@ place `data-pan-after-hold` is.
152
165
 
153
166
  ### When the surface has the hand: `grab`, `release`, `[data-grabbed]`
154
167
 
155
- A surface that is asked for — by a hold, or by the first few pixels of travel —
156
- has an instant where it becomes the hand's, and nothing on screen says it. The
157
- hand then moves too early and scrolls the page instead, or waits long past the
158
- moment out of doubt. So the surface says it, the same two words a carried element
159
- says (`grab`, `release`) and the same attribute:
168
+ A surface that is asked for — by a hold, by the first few pixels of travel, or
169
+ 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:
160
173
 
161
174
  ```jsx
162
175
  <Box
@@ -180,7 +193,8 @@ says (`grab`, `release`) and the same attribute:
180
193
  }
181
194
  ```
182
195
 
183
- `[data-grabbed]` is on the element for as long as a hand is on the surface, so
196
+ `[data-grabbed]` is on the element for as long as a hand is on the surface (on
197
+ one that only zooms, from the second finger landing to the last one lifting), so
184
198
  a contour, a veil or a raised shadow needs no listener — which is what this is
185
199
  usually for. (`[data-hand-kept]` is the other one, and it outlasts the finger:
186
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
package/docs/resource.md CHANGED
@@ -42,6 +42,7 @@ looks like ([data_states.md](./data_states.md#data-and-loading-are-independent))
42
42
  - [`.many(propertyName, childResource, restCallbacks)`](#manypropertyname-childresource-restcallbacks)
43
43
  - [`.scopedOne(propertyName, { idKey, GET, POST, PUT, PATCH, DELETE })`](#scopedonepropertyname--idkey-get-post-put-patch-delete-)
44
44
  - [`.scopedMany(propertyName, { idKey, GET, GET_MANY, POST, PUT, PATCH, DELETE, … })`](#scopedmanypropertyname--idkey-get-get_many-post-put-patch-delete--)
45
+ - [A relation declared on a scoped child](#a-relation-declared-on-a-scoped-child)
45
46
  - [When the backend answers a sub-route with the whole parent](#when-the-backend-answers-a-sub-route-with-the-whole-parent)
46
47
  - [Relations and autorerun](#relations-and-autorerun)
47
48
  - [`withParams()`: a scope with reruns of its own](#withparams-a-scope-with-reruns-of-its-own)
@@ -256,7 +257,8 @@ These are not guessable — each relationship method has its own shape.
256
257
 
257
258
  ### `.one(propertyName, childResource, { GET, PUT, DELETE })`
258
259
 
259
- The callback returns the **parent** object with the child nested inside:
260
+ `GET` and `PUT` return the **parent** object with the child nested inside;
261
+ `DELETE` returns the parent id, or `{ id }`:
260
262
 
261
263
  ```js
262
264
  const USER_SESSION = USER.one("session", SESSION, {
@@ -358,6 +360,43 @@ const GAME_CANDIDATES = GAME.scopedMany("candidates", {
358
360
  `idKey` names the child's own key inside its owner (`user_id` above); it defaults
359
361
  to `"id"`.
360
362
 
363
+ ### A relation declared on a scoped child
364
+
365
+ A scoped child takes relations of its own — a column pointing at a shared data
366
+ type:
367
+
368
+ ```js
369
+ const TABLE_COLUMNS = TABLE.scopedMany("columns", {
370
+ idKey: "name",
371
+ GET_MANY: async ({ id }) => [id, await fetchJson(`/tables/${id}/columns`)],
372
+ // [id, [{ name: "email", dataType: { id: 2, name: "varchar" } }, …]]
373
+ PATCH: async ({ id, name, ...props }) => [
374
+ id,
375
+ await fetchJson(`/tables/${id}/columns/${name}`, {
376
+ method: "PATCH",
377
+ body: props,
378
+ }),
379
+ ],
380
+ });
381
+ TABLE_COLUMNS.one("dataType", DATA_TYPE);
382
+ ```
383
+
384
+ Every column gets a reactive `dataType` read from the `DATA_TYPE` store, fed by
385
+ what the scoped child's callbacks embed. The relation itself takes none of the
386
+ callbacks whose result names the column — `.one()`'s `GET`/`PUT`/`DELETE`,
387
+ `.many()`'s `GET_MANY`/`DELETE`/`DELETE_MANY`, and every callback of a
388
+ `.scopedOne()`/`.scopedMany()`, whose `ownerId` would be the column: they name
389
+ it by its `name` alone, and a column exists only inside its table — two tables
390
+ can each have an `email`. Declaring one throws. A column's data type is read and
391
+ changed through `TABLE_COLUMNS`, whose results say which table
392
+ (`[id, { name, dataType }]`); the fields of a data type itself, through
393
+ `DATA_TYPE`.
394
+
395
+ A `.scopedOne()`/`.scopedMany()` chained on a scoped child works the same way,
396
+ and keeps one value per column: `TABLE_COLUMNS.scopedMany("constraints")` gives
397
+ `users.email` and `admins.email` a collection each, fed by the `constraints`
398
+ embedded in each table's columns.
399
+
361
400
  ## When the backend answers a sub-route with the whole parent
362
401
 
363
402
  This is the common REST shape, and it is the reason `op` dispatch feels
@@ -404,7 +443,8 @@ parent's own fields change too, either let the parent GET rerun (see below) or
404
443
  Relationship mutations do **not** invalidate their parent by default. The exact
405
444
  rules, verified by `src/state/rest/tests/resource_graph_parent_rerun.test.js`:
406
445
 
407
- - `.scopedMany` child **POST** reruns the owner's singular `GET` — but only when
446
+ - `.scopedMany` child **POST** reruns the owner's singular `GET`, whether it
447
+ goes through the child or through a `withParams()` scope of it — but only when
408
448
  the last GET response actually embedded that property. GET_MANY on the parent
409
449
  is never rerun by a child POST (a list of parents is not stale because one of
410
450
  them gained a child).
@@ -419,7 +459,8 @@ rules, verified by `src/state/rest/tests/resource_graph_parent_rerun.test.js`:
419
459
  rerun) by its own `DELETE`, as a root `GET` is; a scoped `GET` is not reset —
420
460
  its data simply resolves to nothing once the child is dropped. Override per
421
461
  relation with `rerunOn`/`dependencies`, which every relationship method
422
- accepts.
462
+ accepts; what a relation leaves out is its parent resource's (`rerunOn` key
463
+ by key).
423
464
 
424
465
  Splitting a sub-resource out of a parent `PATCH` therefore changes the refresh
425
466
  graph: a parent field the parent's own response kept fresh is, once the relation
@@ -457,11 +498,18 @@ share is the **store**: an item updated through one is the same object in the
457
498
  others, so the fields of a row change everywhere without a request — only the
458
499
  membership of each list is a question its own scope answers.
459
500
 
501
+ It scopes a relation as well as a resource, and the scope's actions are the
502
+ relation's: `TABLE_COLUMNS.withParams({ withTypes: true }).GET_MANY` keeps the
503
+ `[ownerId, items]` contract of `.scopedMany()` and replaces that table's
504
+ columns.
505
+
460
506
  `withParams()` chains, merging the params —
461
507
  `USER.withParams({ role: "admin" }).withParams({ gender: "male" })` is
462
508
  `USER.withParams({ role: "admin", gender: "male" })`. Its second argument takes
463
- `rerunOn` and `dependencies` for that scope; left out, the scope inherits the
464
- resource's. Empty params throw: a scope has to be about something.
509
+ `rerunOn` and `dependencies` for that scope; what it leaves out is the
510
+ resource's — a `rerunOn` key by key, so `{ rerunOn: { GET: ["PATCH"] } }` keeps
511
+ the resource's `GET_MANY` and `GET_RANGE`. Empty params throw: a scope has to be
512
+ about something.
465
513
 
466
514
  It is not the tool for a param the user types. A search word bound with
467
515
  `withParams()` would keep its results from refreshing after a write on the
package/docs/safe_area.md CHANGED
@@ -75,13 +75,8 @@ So the app states its own screen once, and never names a component:
75
75
 
76
76
  The bands fall out of it (centered), `--navi-app-width` follows, and `FixedBar`
77
77
  pins itself to the column's edges rather than the glass. An app wanting them
78
- uneven writes `--navi-app-inset-left` / `-right` directly instead.
79
-
80
- In pixels: popup placement reads this value back from CSS to compute its own
81
- margins, and a custom property computes to a token stream rather than to a
82
- length, so `40rem` would arrive there as the string `"40rem"`. A non-px value
83
- still caps the popup's size (that part is pure CSS) but leaves its margins and
84
- its placement viewport-sized, and says so in the console.
78
+ uneven writes `--navi-app-inset-left` / `-right` directly instead; everything
79
+ below follows those the same way, popup placement included.
85
80
 
86
81
  Every popup follows: `Dialog`, `Popover`, and everything built on them
87
82
  (`Picker`, `Select`…). It is a ceiling and nothing more — on a screen narrower
@@ -116,15 +111,19 @@ price.
116
111
 
117
112
  #### Placement follows the same rectangle
118
113
 
119
- `--navi-app-max-width` moves where a popup is placed, not only how big it may
114
+ The app's rectangle moves where a popup is placed, not only how big it may
120
115
  get. Placement is computed against the visual viewport narrowed to the level-1
121
- bands: `getAppInsets` (`src/layout/responsive.js`) is the JS reading of them,
122
- navi hands them to `@jsenv/dom` once (`setPlacementViewportInsets`, wired in
116
+ bands: `getAppInsets` (`src/layout/responsive.js`) reads `--navi-app-inset-*`
117
+ back off the computed style — the bands `--navi-app-max-width` centers and the
118
+ ones an app writes by hand alike, in any length unit — navi hands them to
119
+ `@jsenv/dom` once (`setPlacementViewportInsets`, wired in
123
120
  `navi_css_vars.js`), and `pickPositionRelativeTo` reads them on every
124
- placement. Invisible for anything centered on its cross axis — `center`,
125
- `bottom`, `top`, which is what a dialog does nearly always — but it is what puts
126
- anything anchored to an edge (a `positionArea` like `bottom-start`, a
127
- `SidePanel`) flush against the app column's edge rather than the window's.
121
+ placement. With centered bands it is invisible for anything centered on its
122
+ cross axis — `center`, `bottom`, `top`, which is what a dialog does nearly
123
+ always — and with uneven ones it is what centers that dialog on the app column
124
+ rather than on the window. Either way it is what puts anything anchored to an
125
+ edge (a `positionArea` like `bottom-start`, a `SidePanel`) flush against the
126
+ app column's edge rather than the window's.
128
127
  `FixedBar` reads the same description through CSS instead: it is pinned to
129
128
  `--navi-app-inset-*`, which says where the app's rectangle is in the window
130
129
  rather than how wide it may be.
@@ -156,20 +155,25 @@ Beware of making that container scrollable by accident — see
156
155
  `var(--navi-safe-area-inset-bottom)` in any rule. It is always declared, whether
157
156
  or not the app ever mounts a bar.
158
157
 
159
- From **JS**, the four `--navi-safe-area-inset-*` are registered as lengths
160
- (`@property`, in `safe_area.js`), so
158
+ From **JS**, both levels are registered as lengths (`@property`, in
159
+ `safe_area.js`), so
161
160
  `getComputedStyle(document.documentElement).getPropertyValue("--navi-safe-area-inset-bottom")`
162
- gives pixels. The level-1 `--navi-app-inset-*` are not registered and keep their
163
- `calc()` unresolved there: give a hidden box `height: var(…)` and measure it —
164
- see `src/layout/demos/fixed_bar/keyboard.html`.
161
+ gives pixels, and so does `--navi-app-inset-*`.
165
162
 
166
163
  ### Putting something new into it
167
164
 
168
- Publish what you take on one edge, into that edge's slot. That is the whole
169
- contract — a native banner, an OS strip, anything an app invents joins the sum
170
- without a single component learning it exists. `FixedBar` is the worked example:
171
- it measures its own border box (notch included) and writes
172
- `--navi-fixed-bar-space-*` (`src/layout/fixed_bar/fixed_bar_space.js`).
165
+ Level 2 has one slot per edge, `--navi-fixed-bar-space-*`, and it belongs to
166
+ `FixedBar`: each bar measures its own border box (notch included), and navi
167
+ writes the largest one on that edge inline on `<html>`
168
+ (`src/layout/fixed_bar/fixed_bar_space.js`). The slot is combined with
169
+ `env(safe-area-inset-*)` through `max()`, not added to it, since a bar measured
170
+ with its notch already covers the notch.
171
+
172
+ Something else taking an edge — a native banner, an OS strip — is published by
173
+ being drawn as a `FixedBar`; every component reading the safe area then clears
174
+ it without learning it exists. Writing the slot by hand holds only while no bar
175
+ takes that edge: the first bar mounting there replaces the value rather than
176
+ adding to it.
173
177
 
174
178
  ## What already reads it
175
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.388",
3
+ "version": "0.29.390",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "preact": "^11.0.0-rc.2"
35
35
  },
36
36
  "dependencies": {
37
- "@jsenv/dom": "0.17.72",
37
+ "@jsenv/dom": "0.17.74",
38
38
  "@jsenv/humanize": "1.8.5",
39
39
  "@jsenv/validity": "0.4.5"
40
40
  },