@jsenv/navi 0.29.83 → 0.29.85
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 +202 -32
- package/dist/jsenv_navi.js.map +12 -7
- package/docs/AI_INSTRUCTIONS.md +3 -1
- package/docs/control_object.md +83 -0
- package/docs/control_value.md +13 -0
- package/docs/navigation.md +39 -3
- package/package.json +1 -1
package/docs/AI_INSTRUCTIONS.md
CHANGED
|
@@ -204,7 +204,9 @@ consistency across the app, not from any single call site.
|
|
|
204
204
|
than a param, search params bound to signals, rendering with `<Route>`,
|
|
205
205
|
tab rows (`Nav` / `Link` / `RouteTravel`), where a navigation lands
|
|
206
206
|
(scroll: a push arrives at the top, a back or forward lands where the page
|
|
207
|
-
was left),
|
|
207
|
+
was left), a back arrow that stays inside the app (`useCanNavBack()` /
|
|
208
|
+
`navBack({ fallback })`), and the few cases where tabs are legitimately not
|
|
209
|
+
URLs. Read it before writing any routing code — the
|
|
208
210
|
position of the user belongs in the URL by default, and that decision is
|
|
209
211
|
not retrofittable.
|
|
210
212
|
- Source code and demos on GitHub:
|
package/docs/control_object.md
CHANGED
|
@@ -11,6 +11,7 @@ whose value is an object needs in its popup.
|
|
|
11
11
|
- [`<Form>`: the shape, plus a send](#form-the-shape-plus-a-send)
|
|
12
12
|
- [Naming, and what a nameless group does](#naming-and-what-a-nameless-group-does)
|
|
13
13
|
- [A picker whose value is an object](#a-picker-whose-value-is-an-object)
|
|
14
|
+
- [A settings sheet](#a-settings-sheet)
|
|
14
15
|
- [`Group` is not `ControlGroup`](#group-is-not-controlgroup)
|
|
15
16
|
|
|
16
17
|
## `<ControlGroup>`: the shape
|
|
@@ -113,6 +114,88 @@ Two things to get right:
|
|
|
113
114
|
object lands on one control, which is how `"[object Object]"` ends up in a
|
|
114
115
|
url.
|
|
115
116
|
|
|
117
|
+
## A settings sheet
|
|
118
|
+
|
|
119
|
+
A popup that is not one choice but a handful of settings — four tabs, a select,
|
|
120
|
+
a field, a button that answers with a place — and ONE answer, which must reach
|
|
121
|
+
the app only when the popup closes. The list behind it must not move while it is
|
|
122
|
+
open, and Escape must leave things exactly as they were found.
|
|
123
|
+
|
|
124
|
+
Nothing new is needed for that: it is the object picker above, with the group in
|
|
125
|
+
its popup. A picker's `action` runs on close and only on close (its `uiAction`
|
|
126
|
+
follows every gesture, which is what the popup shows), so what is inside acts on
|
|
127
|
+
nothing until the user is done.
|
|
128
|
+
|
|
129
|
+
```jsx
|
|
130
|
+
<Picker
|
|
131
|
+
type="object"
|
|
132
|
+
mode="dialog"
|
|
133
|
+
value={zone}
|
|
134
|
+
ui={<ZoneSummary zone={zone} />}
|
|
135
|
+
action={save}
|
|
136
|
+
>
|
|
137
|
+
<ControlGroup>
|
|
138
|
+
<Nav slideContainer="zone_slides" currentIndicator>
|
|
139
|
+
<Link slide="city">Ville</Link>…
|
|
140
|
+
</Nav>
|
|
141
|
+
<Input type="hidden" name="origin" signal={originSignal} />
|
|
142
|
+
<SlideContainer id="zone_slides" signal={originSignal}>
|
|
143
|
+
<Slide area="city">
|
|
144
|
+
<Input name="city" />
|
|
145
|
+
</Slide>
|
|
146
|
+
…
|
|
147
|
+
</SlideContainer>
|
|
148
|
+
</ControlGroup>
|
|
149
|
+
</Picker>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// what `save` receives, on close and once
|
|
154
|
+
{ origin: "city", city: "Antibes", radius: "30", department: "" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Which tab is showing is part of the answer.** The same fields mean different
|
|
158
|
+
things depending on the tab they were filled on, so the tab is a key of the
|
|
159
|
+
value like the rest — a sheet handing back `{ city: "Antibes" }` without saying
|
|
160
|
+
which tab it was left on has not said what was chosen. A tab bar is a navigation
|
|
161
|
+
(`<Nav slideContainer>` + `<Link slide>`), so nothing in it is a field: the
|
|
162
|
+
current area reaches the value through an `<Input type="hidden">` bound to the
|
|
163
|
+
same signal as the container. That is what a hidden field is for — a
|
|
164
|
+
piece of the answer with no control to be read from, here because the tabs are
|
|
165
|
+
places rather than choices. A tab bar made of radios needs none: it is a field
|
|
166
|
+
already, under its own name.
|
|
167
|
+
|
|
168
|
+
That signal is written in both directions, which is what makes the sheet reopen
|
|
169
|
+
where it was left: a value handed DOWN to a control writes it too — on open, and
|
|
170
|
+
when Escape puts back what the picker held — so the tabs go back to the answer
|
|
171
|
+
rather than staying on the one that was being tried.
|
|
172
|
+
|
|
173
|
+
**Every field answers, including the ones the current tab does not use.** A
|
|
174
|
+
field on another tab stays mounted, keeps what was typed in it, and comes back
|
|
175
|
+
in the object. Which of them count is read from `origin` by whoever receives the
|
|
176
|
+
value — the group has no opinion about it, and does not need one. Deriving
|
|
177
|
+
something narrower (a stored string, an id) is that reader's business too:
|
|
178
|
+
`action` receives the object and stores whatever it wants, `value` hands the
|
|
179
|
+
object back.
|
|
180
|
+
|
|
181
|
+
A group CAN be worth a single value of its own — `aggregateChildStates` and
|
|
182
|
+
`distributeChildUIState`, see the top of this file — and a sheet is precisely
|
|
183
|
+
where that is the wrong reach: it throws away the state that makes the answer
|
|
184
|
+
readable, starting with the tab.
|
|
185
|
+
|
|
186
|
+
**A button inside is not a field.** The one asking for a position acts on the
|
|
187
|
+
press, like anywhere else; what it answers goes into a control that IS a field
|
|
188
|
+
(a read-only one showing the place it found), and the group picks it up from
|
|
189
|
+
there.
|
|
190
|
+
|
|
191
|
+
The façade stays on what is saved by reading the app's own state
|
|
192
|
+
(`value={zone}` + a `ui` rendered from `zone`), while the picker holds the
|
|
193
|
+
draft: the summary behind the open dialog then shows the answer, not the
|
|
194
|
+
attempt.
|
|
195
|
+
|
|
196
|
+
Seen working — the tabs, the geolocation button, the cancel — in
|
|
197
|
+
`control/demos/picker/9_picker_settings_sheet_demo.html`.
|
|
198
|
+
|
|
116
199
|
## `Group` is not `ControlGroup`
|
|
117
200
|
|
|
118
201
|
Two names, two subjects, no relation:
|
package/docs/control_value.md
CHANGED
|
@@ -46,6 +46,15 @@ const minutesSignal = useSignal(0);
|
|
|
46
46
|
minutesSignal.value = 30;
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
"Every change" includes the ones the control did not decide: a group placing
|
|
50
|
+
its children, a picker filling its popup at open, and the same picker putting
|
|
51
|
+
back what it held when Escape cancels. The signal mirrors the control, so it
|
|
52
|
+
always says where the control actually is — which is what lets a settings sheet
|
|
53
|
+
reopen on the tab it was left on (see
|
|
54
|
+
[control_object.md](./control_object.md#a-settings-sheet)). What it does NOT
|
|
55
|
+
mirror is a picker's popup being played with: a picker's own signal is written
|
|
56
|
+
when the picker commits, on close.
|
|
57
|
+
|
|
49
58
|
Both halves are worth knowing about, because each replaces a habit:
|
|
50
59
|
|
|
51
60
|
- the write-back replaces `uiAction={(v) => (mySignal.value = v)}`;
|
|
@@ -115,6 +124,10 @@ All of them: `Input` (every type), `Picker`, `Select`, `Wheel`, `Spin`,
|
|
|
115
124
|
that is a navi control goes through the same state controller, and the same
|
|
116
125
|
`signal` prop.
|
|
117
126
|
|
|
127
|
+
`SlideContainer` takes one too, though it is layout rather than a control: the
|
|
128
|
+
area it shows is a piece of state like any other, and binding it is what lets
|
|
129
|
+
something else read where the slides are — or move them by writing it.
|
|
130
|
+
|
|
118
131
|
Inside a `List selectable` you can bind the list, or give each `List.Item` its
|
|
119
132
|
own `selected` — but not expect the two to arbitrate. An item that declares
|
|
120
133
|
`selected` is answering for itself, and the list's signal does not reposition
|
package/docs/navigation.md
CHANGED
|
@@ -260,9 +260,45 @@ The link stays a link — an address, a middle click, the keyboard, `aria-curren
|
|
|
260
260
|
true })` and `route.redirectTo()`.
|
|
261
261
|
|
|
262
262
|
A replaced entry inherits the state of the one it takes the place of (so does
|
|
263
|
-
`route.redirectTo()`): **an entry's state does not say how it arrived**.
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
`route.redirectTo()`): **an entry's state does not say how it arrived**. Only
|
|
264
|
+
the navigation being applied says that, and navi is the one applying it — see
|
|
265
|
+
the back arrow below, which is what that fact is usually needed for.
|
|
266
|
+
|
|
267
|
+
## The back arrow: `navBack`
|
|
268
|
+
|
|
269
|
+
An arrow drawn inside the app promises the screen the reader came from — never
|
|
270
|
+
the page they were on before the app. Both cases are real for the same url: one
|
|
271
|
+
descends into a profile from a list, or one opens it cold from a shared link, a
|
|
272
|
+
bookmark, a notification. `history.back()` answers the first and, on the second,
|
|
273
|
+
gives back the conversation the link came from.
|
|
274
|
+
|
|
275
|
+
`window.history.length` cannot tell them apart — it counts the whole tab — and
|
|
276
|
+
neither can an entry's state, for the reason just above. What answers is a count
|
|
277
|
+
of how many entries of THIS document stand underneath, kept as the navigations
|
|
278
|
+
are applied and written into each entry so it survives a reload mid-stack. navi
|
|
279
|
+
keeps it: an app that kept its own would have to be told about every single
|
|
280
|
+
`replace` it performs, and the one it forgets shows up only on a cold-opened
|
|
281
|
+
screen after a precise gesture.
|
|
282
|
+
|
|
283
|
+
```jsx
|
|
284
|
+
const BackButton = () => {
|
|
285
|
+
const canNavBack = useCanNavBack();
|
|
286
|
+
...
|
|
287
|
+
};
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`useCanNavBack()` (or `canNavBackSignal` outside a component) is reactive: the
|
|
291
|
+
arrow appears and disappears as the stack moves, it is not decided at mount.
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
navBack({ fallback: USER_ME_ROUTE.buildUrl() });
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The `fallback` takes the place of the current entry rather than stacking on it:
|
|
298
|
+
pushed, it would put the screen just left one press ahead, and the phone's own
|
|
299
|
+
back button would walk straight back into it — a loop with no way out of the
|
|
300
|
+
app. Without a `fallback`, a `navBack()` with nothing of ours behind does
|
|
301
|
+
nothing.
|
|
266
302
|
|
|
267
303
|
## Tabs that travel: `RouteTravel`
|
|
268
304
|
|