@jsenv/navi 0.29.101 → 0.29.103

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.
@@ -123,11 +123,24 @@ consistency across the app, not from any single call site.
123
123
  inside a `Dialog`/`Popover`, and what a scroll does to hover
124
124
  (`hoverWhileScrolling`, `isScrolling()`). Read it before writing CSS to make
125
125
  something scroll — navi almost certainly already has the prop.
126
+ - `docs/badge_list.md` — what a `BadgeList` does per prop (plain, `max`,
127
+ `shrinkWrap`, `maxLines`) and how it counts its badges (a `Badge` inside one
128
+ renders nothing and hands itself to the list — so badges must be direct
129
+ children). How `maxLines` reaches a list drawn inside a `Picker`
130
+ (`MaxLinesContext`, the picker's own clamp turned off), and why a list in a
131
+ picker needs a `fallback` — the placeholder text, as plain text: a picker
132
+ given a `ui` draws no placeholder of its own. Read it before putting a
133
+ `BadgeList` in a picker's `ui`, before wrapping its fallback in a `Badge`,
134
+ and before setting `maxLines` on the list rather than on the picker.
126
135
  - `docs/control_value.md` — who holds a control's value: nobody, a bound
127
136
  `signal` (two-way, in both directions), or you (`value`/`checked`). What
128
137
  `signal` + `defaultValue` says, what a signal holds for each kind of control,
129
- and why `value` and `signal` cannot both be passed. Read it before wiring a
130
- control's value by hand with `value` + `uiAction`.
138
+ and why `value` and `signal` cannot both be passed. It also holds the answer
139
+ to "a shortcut button beside a control" — `--navi-update`, gated like every
140
+ interaction, against an `onClick` writing the signal, which is not and fires
141
+ on a read-only control. Read it before wiring a
142
+ control's value by hand with `value` + `uiAction`, and before writing a button
143
+ that proposes a value.
131
144
  - `docs/create_and_edit.md` — the loop almost every app has: a screen that
132
145
  creates a resource, the page of what was created, a screen that edits it. The
133
146
  routes and why several match at once, one form for two modes, filling the edit
@@ -203,9 +216,12 @@ consistency across the app, not from any single call site.
203
216
  of navi's own.
204
217
  - `docs/interactions.md` — the `interactions` prop: making a component answer a
205
218
  swipe, a held press, a shortcut, and registering a gesture navi does not have.
219
+ It also holds `ownTarget`, for an affordance an application draws inside a
220
+ zone that belongs to another control — a chip's cross, an eye, a diskette.
206
221
  Read it before reading the pointer by hand — who owns a press between nested
207
222
  boxes, and what a touch may do, are decided before the first pixel moves and
208
- cannot be got right from outside navi.
223
+ cannot be got right from outside navi — and before stopping the propagation of
224
+ a pointerdown/mousedown/click to keep a popup from opening.
209
225
  - `docs/drag_to_travel.md` — a pointer pushing a whole screen aside
210
226
  (`SlideContainer`, `RouteTravel`) and a popup pushed back towards its edge:
211
227
  what the gesture is, and above all who owns a press several boxes want — a
@@ -0,0 +1,80 @@
1
+ # BadgeList
2
+
3
+ A row of badges that wraps. What it does at runtime depends on what it is asked
4
+ for, and nothing is set up for a case that cannot happen:
5
+
6
+ - **plain** — no `max`, no `maxLines`, no `fallback`, no `shrinkWrap`: one
7
+ element holding its children as-is. No registry, no effect, no measurement.
8
+ - **`max` / `fallback`** — the badges are counted (see below), nothing is
9
+ measured, no DOM is watched.
10
+ - **`shrinkWrap`** — a hidden clone of the list is laid out to find the widest
11
+ wrapped row, and the list is narrowed to it so the last row isn't ragged.
12
+ Opt-in outside a `Picker` (a picker draws a border around the list, so the
13
+ ragged edge shows; elsewhere the work would often go unseen), and skipped
14
+ whenever `maxLines` is in play.
15
+ - **`maxLines`** — every badge is laid out once, hidden; where the rows fell is
16
+ read; the list is rendered again with the badges that fit and a `+N more`
17
+ badge for the rest. Both renders land in the same frame. A width change of
18
+ the room around the list triggers another measure — not a change of the
19
+ list's own width, which is what it produces, not what it is given.
20
+
21
+ ## How badges are counted
22
+
23
+ A `Badge` inside a `BadgeList` does not render itself: it hands its props to
24
+ the list through `BadgeListContext` and renders nothing. Badges register in
25
+ tree order, so by the time the list gets to its own content it holds them all
26
+ and knows how many there are before deciding what to show — without walking
27
+ children vnodes, and without rendering a badge it then has to take back.
28
+
29
+ Two consequences:
30
+
31
+ - a `Badge` must be a **direct** child of the list — its rendering is moved
32
+ into the list, so `<div><Badge /></div>` takes the badge out of its wrapper;
33
+ - a badge's own `key` goes to the registering vnode and not to the badge the
34
+ list draws, which is keyed by position. Reordering recreates the nodes rather
35
+ than moving them.
36
+
37
+ ## `maxLines` in a Picker
38
+
39
+ A picker clamps its value with `maxLines`, which is CSS line-clamp — it counts
40
+ line boxes of inline text and never sees a wrapped flex row. So a `BadgeList`
41
+ rendered as a picker's `ui` reads the number from `MaxLinesContext` instead
42
+ (the picker provides it, default 1) and caps its own rows to it, and the picker
43
+ turns its own clamp off (`.navi_picker_value:has(.navi_badge_list)`). Nothing
44
+ to set on the list; `maxLines` on the picker is enough, and a `maxLines` on the
45
+ list itself is a local override.
46
+
47
+ `max` is a different cap: a number of badges, whatever the rows. The two
48
+ compose — the `+N` badge takes one of the `max` slots.
49
+
50
+ ## The `fallback`
51
+
52
+ Without a `fallback`, an empty list renders **nothing**. Inside a picker there
53
+ is one more thing to know: a picker given a `ui` draws that and only that, its
54
+ own `placeholder` is **not** drawn next to it. An empty `BadgeList` in a picker
55
+ is a blank picker — so the list's `fallback` is the placeholder, and the
56
+ placeholder text is what to pass:
57
+
58
+ ```jsx
59
+ <BadgeList fallback="Select skills…">
60
+ {selected.map((skill) => (
61
+ <Badge key={skill}>{skill}</Badge>
62
+ ))}
63
+ </BadgeList>
64
+ ```
65
+
66
+ Plain text is the right shape for it. It reads at the picker's own size, in the
67
+ placeholder color — the picker colors its value slot that way while it holds
68
+ nothing — and the picker box stays the same height empty and filled: the box is
69
+ held one line tall by the picker's own line (the right slot is `1lh`), whatever
70
+ the slot inside holds. That slot is a fraction of a pixel taller with a line of
71
+ text than with a row of badges (0.89px at the default font size — a line is set
72
+ by the picker's font and line-height, a badge by its own padding and smaller
73
+ font), which nothing shows.
74
+
75
+ A transparent `Badge` as fallback matches the slot to the pixel instead, but its
76
+ text is badge-sized — smaller than the picker's — and reads as a badge with
77
+ nothing in it. Only worth it where the slot itself is what something else is
78
+ sized on. Section 4 of `src/control/demos/picker/2_select_multiple_demo.html`
79
+ measures the three cases (no fallback, plain text, transparent badge), box and
80
+ slot side by side.
@@ -47,14 +47,19 @@ Live examples: `src/control/demos/15_group_demo.html`.
47
47
  last one loses it on the other side, and any member in between loses all
48
48
  four. A single member keeps its own radius — a group of one looks like the
49
49
  control alone. The ask is made twice, in two forms — see below.
50
- - **Overlap order**: the member under the pointer, and the member showing a
51
- focus ring, paint above their neighbours (`position: relative` plus
52
- `--navi-z-index-control-hovered` / `-focused`). Without it the border color
53
- change and the focus ring of the active member would be sliced by whichever
54
- neighbour is painted after it. The focused member is matched whether it wears
55
- `data-focus-visible` itself or merely contains it a control that wraps a
56
- real input (`Picker`, `Spin`) draws the ring on its own frame while the
57
- keyboard is held inside. There is deliberately no `isolation: isolate` see
50
+ - **Overlap order**: the member under the pointer, the member showing a focus
51
+ ring, and the member holding something open (`aria-expanded="true"`) paint
52
+ above their neighbours (`position: relative` plus
53
+ `--navi-z-index-control-hovered` / `-focused` / `-expanded`). Without it the
54
+ border color change and the focus ring of the active member would be sliced
55
+ by whichever neighbour is painted after it. The focused member is matched
56
+ whether it wears `data-focus-visible` itself or merely contains it a
57
+ control that wraps a real input (`Picker`, `Spin`) draws the ring on its own
58
+ frame while the keyboard is held inside; the expanded member is matched the
59
+ same way, since it can be wrapped in an enrobage. Expanded ranks highest: a
60
+ `Picker` opened by a click shows no focus ring, hands the focus to its popup
61
+ and lets the pointer travel to a neighbour, yet its border keeps saying it is
62
+ the one open. There is deliberately no `isolation: isolate` — see
58
63
  [z_index.md](./z_index.md).
59
64
 
60
65
  Nothing else: a group does not restyle its members, does not impose a size,
@@ -113,7 +118,7 @@ opened it. `Popover` and `Dialog` stop it too, at their own root: nothing a
113
118
  popup holds is at a seam.
114
119
 
115
120
  Reference: `.navi_button_content` in `button_ui.jsx` (a button reads then
116
- stops), `.navi_picker_right_slot`, `.navi_input_slot`, `.navi_popover`,
121
+ stops), `.navi_picker_box`, `.navi_input_slot`, `.navi_popover`,
117
122
  `.navi_dialog`.
118
123
 
119
124
  A control that declares its radius on an inner element instead is invisible to
@@ -6,6 +6,7 @@ somewhere else in the app.
6
6
 
7
7
  - [The three answers](#the-three-answers)
8
8
  - [A bound signal works in both directions](#a-bound-signal-works-in-both-directions)
9
+ - [A button that proposes a value is `--navi-update`](#a-button-that-proposes-a-value-is---navi-update)
9
10
  - [`signal` + `defaultValue`: the answer and where it starts](#signal--defaultvalue-the-answer-and-where-it-starts)
10
11
  - [What a signal holds, control by control](#what-a-signal-holds-control-by-control)
11
12
  - [Empty keeps the shape of the question](#empty-keeps-the-shape-of-the-question)
@@ -68,20 +69,64 @@ Both halves are worth knowing about, because each replaces a habit:
68
69
  The follow goes all the way up: a bound control that lives inside a group — two
69
70
  wheels in a `WheelGroup`, a field in a `ControlGroup` — makes that group
70
71
  re-aggregate when its signal is written, and the form above sees the new value.
71
- A shortcut that pushes the controls from the outside is an answer like any
72
- other: the wheels roll, and the submit lights up.
72
+ A value pushed in from anywhere is an answer like any other: the wheels roll,
73
+ and the submit lights up. Which is why a **button** offering such a value is not
74
+ a hand-written signal write — see the next section.
75
+
76
+ ## A button that proposes a value is `--navi-update`
77
+
78
+ A shortcut beside a control — "Tous niveaux" / "Aucun niveau" next to a list of
79
+ levels, "1h / 1h30 / 2h" next to a pair of wheels, a suggestion under a field —
80
+ is a value being offered to that control. It is not an action, and it is not a
81
+ signal to write by hand:
73
82
 
74
83
  ```jsx
84
+ <ControlGroup id="duration">
85
+ <TimeWheel name="duration" signal={durationSignal} />
86
+ </ControlGroup>
87
+
88
+ <Button
89
+ command="--navi-update"
90
+ commandFor="duration"
91
+ value={{ hours: 1, minutes: 30 }}
92
+ >
93
+ 1h30
94
+ </Button>
95
+ ```
96
+
97
+ - the **value** is the button's own `value`, whatever shape it has — a string, an
98
+ array of levels, an object of two wheels;
99
+ - the **target** is `commandFor`, naming the control's id — left out, the nearest
100
+ control around the button is used, which is what a button placed inside the
101
+ control it proposes to wants;
102
+ - and the press goes through the same gate as every other interaction, so a
103
+ read-only, disabled or busy control **refuses it and says why**.
104
+
105
+ That last point is the whole reason, and the counter-example is what everybody
106
+ writes first:
107
+
108
+ ```jsx
109
+ // ✗ not gated — plain DOM. On a read-only sheet the button greys out and fires
110
+ // all the same, rewriting a value nobody is allowed to change.
75
111
  <Button
76
112
  onClick={() => {
77
- hoursSignal.value = 2;
78
- minutesSignal.value = 0;
113
+ durationSignal.value = { hours: 1, minutes: 30 };
79
114
  }}
80
115
  >
81
- 2h
116
+ 1h30
82
117
  </Button>
83
118
  ```
84
119
 
120
+ Writing a signal from an `onClick` is only right where nothing is being proposed
121
+ to a control: moving something else on screen, seeding state before anything is
122
+ drawn.
123
+
124
+ The id goes **on the control**, and a group is one — `ControlGroup`, `Form`,
125
+ `WheelGroup`, `List selectable`. Put it on a layout box around the control and
126
+ the command finds an element that holds no value; navi says so in dev rather
127
+ than letting the press do nothing at all. An id that matches nothing is a dev
128
+ warning too, naming the id it looked for.
129
+
85
130
  ## `signal` + `defaultValue`: the answer and where it starts
86
131
 
87
132
  They are not competing, they answer two different questions:
@@ -105,6 +105,20 @@ interaction happened, and nothing runs. A `Box` with no control anywhere near it
105
105
  still answers a callback; only `"request_action"` has nothing to ask, and says so
106
106
  in dev.
107
107
 
108
+ The one thing the gate weighs besides the control's state is what the
109
+ interaction would do to it. Everything writes unless it says otherwise; an
110
+ interaction that only shows what is already there declares `intent: "read"`, and
111
+ a control held read-only lets that one through. That is how a read-only
112
+ `<Picker>` still opens: the popup is where its answer is really drawn, so it
113
+ opens and everything inside it is held read-only in turn. Disabled and busy go
114
+ on refusing either way — one is out of service, the other is mid-operation, and
115
+ neither has anything to show.
116
+
117
+ Which controls let a read through is theirs to say, not the caller's: a picker
118
+ with no popup of its own opens the browser's, which cannot be held read-only, so
119
+ that one refuses. `openWhileReadOnly={false}` is how a caller says the popup is
120
+ a form with nothing to read.
121
+
108
122
  ## What a swipe draws, and what it leaves to you
109
123
 
110
124
  navi makes the element follow the finger — there is nothing to decide about
@@ -519,12 +533,23 @@ places and a `toss` wherever it was thrown (`xy`). `data-drag-delay`,
519
533
 
520
534
  ### A control inside something draggable
521
535
 
522
- `data-drag-ignore` says the press there is none of the gesture's business: the
523
- element under it never starts a drag, and keeps both its cursor and its text
524
- selection. That is what a button living inside a carried piece needs a cross
525
- that removes it, a menu otherwise the piece is picked up from the button like
526
- from anywhere else. A click of its own usually has to stop there too, or the
527
- piece reads it as its own click.
536
+ `ownTarget` on the control says it: a chip's cross inside a carried piece, an
537
+ eye on a row that travels, a diskette on a picker's façade. The press belongs to
538
+ that control alone no gesture starts under it, and nothing above it answers
539
+ the mousedown or the click. It also follows the interactivity of the zone around
540
+ it: read-only, disabled or busy, the affordance goes rather than greys (pass
541
+ `ownTarget="refuse"` for one whose presence is information in itself), and its
542
+ `onClick` waits for its own gate instead of firing from the DOM.
543
+
544
+ ```jsx
545
+ <Badge.Button ownTarget onClick={() => remove(id)}>
546
+ ×
547
+ </Badge.Button>
548
+ ```
549
+
550
+ `data-drag-ignore` says the same thing to the gesture alone, for something that
551
+ is not a control: the press there is none of the gesture's business, and the
552
+ element keeps both its cursor and its text selection.
528
553
 
529
554
  ### What says a thing can be picked up
530
555
 
package/docs/z_index.md CHANGED
@@ -74,14 +74,14 @@ file is the overview, this table is its summary. Bands are a decade apart so
74
74
  one can grow without reaching the next, and so a value seen in devtools says
75
75
  which band it came from.
76
76
 
77
- | Band | Token | Value |
78
- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------- |
79
- | Top layer (`Dialog`/`Popover` with `layer="top"`) | — | above everything |
80
- | `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
81
- | `FixedBar` | `--navi-z-index-bar` | 100 |
82
- | Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky` | 10 |
83
- | A `Group` member under the pointer, then the one holding focus | `--navi-z-index-control-hovered`, `--navi-z-index-control-focused` | 1, 2 |
84
- | `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
77
+ | Band | Token | Value |
78
+ | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- |
79
+ | Top layer (`Dialog`/`Popover` with `layer="top"`) | — | above everything |
80
+ | `Dialog`/`Popover` with `layer="local"`, their backdrop, callouts | `--navi-z-index-popup`, `--navi-z-index-callout` | 1000 `+ stack order` |
81
+ | `FixedBar` | `--navi-z-index-bar` | 100 |
82
+ | Sticky while something scrolls under: `List` header/footer/group labels, `SidePanel` head/foot, `Box` header/footer, `<Box sticky>` | `--navi-z-index-sticky` | 10 |
83
+ | A `Group` member under the pointer, then the one holding focus, then the one holding a popup open | `--navi-z-index-control-hovered`, `--navi-z-index-control-focused`, `--navi-z-index-control-expanded` | 1, 2, 3 |
84
+ | `Table` sticky cells, drag, resize | `src/control/table/z_indexes.js` | 1–7, derived from each other |
85
85
 
86
86
  What to read from it:
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.101",
3
+ "version": "0.29.103",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {