Skip to content

Commit bdc8a70

Browse files
committed
work
1 parent fd70c1b commit bdc8a70

9 files changed

Lines changed: 344 additions & 184 deletions

File tree

packages/frontend/navi/dist/jsenv_navi.js

Lines changed: 233 additions & 138 deletions
Large diffs are not rendered by default.

packages/frontend/navi/dist/jsenv_navi.js.map

Lines changed: 9 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/frontend/navi/docs/control_value.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,10 @@ that is a navi control goes through the same state controller, and the same
243243

244244
`SlideContainer` takes one too, though it is layout rather than a control: the
245245
area it shows is a piece of state like any other, and binding it is what lets
246-
something else read where the slides are — or move them by writing it.
246+
something else read where the slides are — or move them by writing it. Bind it
247+
to a `stateSignal` a route declares as a search param and the area is in the
248+
address, with no route per slide — see
249+
[navigation.md](./navigation.md#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from).
247250

248251
Inside a `List selectable` you can bind the list, or give each `List.Item` its
249252
own `selected` — but not expect the two to arbitrate. An item that declares

packages/frontend/navi/docs/css_architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ This means an app can override a Navi default from its own unlayered CSS, withou
4141
}
4242
```
4343

44-
Targeting the same element is not a detail — see [`--navi-*` vs `--component-*`](#--navi-vs---component-where-the-override-has-to-go) below.
44+
Targeting the same element is not a detail — see [`--navi-*` vs `--component-*`](#--navi--vs---component--where-the-override-has-to-go) below.
4545

4646
### Why actual rules stay outside any layer
4747

packages/frontend/navi/docs/navigation.md

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
How to build navigation with `@jsenv/navi`: declaring routes, rendering them,
44
linking to them, and turning them into tabs.
55

6+
- [The rule that decides everything else: the position belongs in the URL](#the-rule-that-decides-everything-else-the-position-belongs-in-the-url)
7+
- [Declaring routes](#declaring-routes)
8+
- [A section is allowed to be a route of its own](#a-section-is-allowed-to-be-a-route-of-its-own)
9+
- [Which values a param accepts](#which-values-a-param-accepts)
10+
- [An address that only sends elsewhere](#an-address-that-only-sends-elsewhere)
11+
- [Search params](#search-params)
12+
- [Rendering routes](#rendering-routes)
13+
- [Loading data](#loading-data)
14+
- [Links and tab rows](#links-and-tab-rows)
15+
- [The back arrow: `navBack`](#the-back-arrow-navback)
16+
- [Tabs that travel: `RouteTravel`](#tabs-that-travel-routetravel)
17+
- [Where a navigation lands: the scroll](#where-a-navigation-lands-the-scroll)
18+
- [Creating something, then editing it](#creating-something-then-editing-it)
19+
- [Tabs that are not routes](#tabs-that-are-not-routes)
20+
- [A `SlideContainer` in the URL: a position that is not a place one came from](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
21+
- [A state whose values ARE places: `history: "push"`](#a-state-whose-values-are-places-history-push)
22+
623
## The rule that decides everything else: the position belongs in the URL
724

825
Where the user is — which section, which tab, which sub-page — is state. Put it
@@ -21,8 +38,10 @@ retrofitted later:
2138
So the default shape of a tab row is routes: `<Nav>` + `<Link route>` +
2239
`<RouteTravel>`. `SlideContainer` is the exception, not the starting point — see
2340
[Tabs that are not routes](#tabs-that-are-not-routes) for the cases that
24-
genuinely are one, and for the middle answer: a position READ from the URL and
25-
restored on reload, without a route and without a history entry per step.
41+
genuinely are one, and
42+
[A `SlideContainer` in the URL](#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from)
43+
for the middle answer: a position READ from the URL and restored on reload,
44+
without a route and without a history entry per step.
2645

2746
## Declaring routes
2847

@@ -609,7 +628,7 @@ page. It reads which slide is on screen from the container itself, and its bar
609628
follows the slides, a finger dragging them included. `<Link slide>` has no href
610629
and behaves like a button: this is not a link to anywhere.
611630

612-
### The middle answer: a position in the URL that is not a place one came from
631+
### A `SlideContainer` in the URL: a position that is not a place one came from
613632

614633
"Should a link be able to open the app on this?" has a third answer, and a wizard
615634
is exactly it: **yes for reading and for reloading, no for history.** The step one
@@ -625,12 +644,12 @@ URL that replaces rather than pushes ([Search params](#search-params)). Declare
625644
the step as one, and hand its signal to the container:
626645

627646
```js
628-
const stepSignal = stateSignal(undefined, {
647+
const stepSignal = stateSignal("when", {
629648
id: "step",
630649
oneOf: ["when", "where", "who", "recap", "done"],
631650
// the step qualifies THIS visit, not the screen: a link built to the editor
632-
// does not inherit the step one happens to be on, and it goes back to nothing
633-
// when the route stops matching
651+
// does not inherit the step one happens to be on, and it goes back to the
652+
// default when the route stops matching
634653
weak: true,
635654
});
636655
export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
@@ -639,16 +658,20 @@ export const ALERT_EDIT_ROUTE = route("/alerts/:alertId/edit", {
639658
```
640659

641660
```jsx
642-
<SlideContainer signal={stepSignal} defaultCurrent={editing ? "recap" : "when"}>
661+
<SlideContainer signal={stepSignal}>
643662
```
644663

645664
That is the whole wiring, and every half of it is the piece that already
646665
existed. Worth naming, because each answers a question a wizard actually has:
647666

648-
- **the param is absent while the step is the default one** (route.js prunes it),
649-
so `/alerts/W-123/edit` stays clean and `defaultCurrent` is what says where the
650-
container opens. An empty signal means "wherever this would have opened
651-
anyway", not "the first slide";
667+
- **where it opens is the state's own default** — the first argument of
668+
`stateSignal`. The container has a `defaultCurrent` for when it owns its
669+
position, but a bound container does not own it: one place says where the step
670+
starts, and it is the same place a reset goes back to;
671+
- **the param is absent while the step IS that default** (route.js prunes it), so
672+
`/alerts/W-123/edit` is clean on the first step, gains `?step=where` on the
673+
second, and loses it again coming back. The address carries what differs from
674+
the usual answer and nothing else;
652675
- **the container walks to the step rather than jumping to it.** The address comes
653676
from outside the box (typed, shared, kept from a session that has moved on), so
654677
every slide between here and there is asked to let go the way a key going that
@@ -664,6 +687,24 @@ A container remembers nothing across a reload, so a step whose `required` the ap
664687
knows is already satisfied says so itself (`required={!alreadyFilled}`); the same
665688
holds for a hold that a finished job lifts (`preventNavNext={!published}`).
666689

690+
**A start that depends on where one is** — a wizard creating something opens on
691+
its first question, the same wizard editing something opens on its summary — is
692+
not a prop on the container either: a `stateSignal` takes a SIGNAL as its default
693+
and follows it as long as nobody has answered otherwise.
694+
695+
```js
696+
const stepDefaultSignal = computed(() =>
697+
// `matchingSignal`, not `matching`: the plain property is a mirror, the
698+
// signal is what a computed can follow
699+
ALERT_EDIT_ROUTE.matchingSignal.value ? "recap" : "when",
700+
);
701+
const stepSignal = stateSignal(stepDefaultSignal, { id: "step", weak: true });
702+
```
703+
704+
The pruning follows it too, so both addresses stay clean on the step they open
705+
on — `/alerts/create` on "when", `/alerts/W-123/edit` on "recap" — and a step
706+
someone actually chose survives the default moving under it.
707+
667708
Two containers on one screen are two signals, and that is the whole answer to
668709
"which one owns the param": the one holding the route's signal. A gallery of
669710
seven wizards side by side hands each of them a `useSignal` of its own, and

packages/frontend/navi/docs/state_binding.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,14 @@ Binding the state does not remove the callback — it removes one job from it:
110110

111111
## What binds itself
112112

113-
| the state | how it binds | where it is written up |
114-
| --------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- |
115-
| a control's value | `signal` (every control, every group) | [control_value.md](./control_value.md) |
116-
| the area a `SlideContainer` is on | `signal` (a route's, and it is in the URL) | its JSDoc, [navigation.md](./navigation.md) |
117-
| a popup being open | `signal`, `open`, `navState` | [popup_open.md](./popup_open.md) |
118-
| where the user is | route + search-param signals | [navigation.md](./navigation.md) |
119-
| a value proposed to a control | `command="--navi-update"` | [control_value.md](./control_value.md#a-button-that-proposes-a-value-is---navi-update) |
120-
| the state of an async run | an `action`, read by `useAsyncData` | [actions.md](./actions.md) |
113+
| the state | how it binds | where it is written up |
114+
| --------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
115+
| a control's value | `signal` (every control, every group) | [control_value.md](./control_value.md) |
116+
| the area a `SlideContainer` is on | `signal` (a route's search param signal puts it in the URL) | its JSDoc, [navigation.md](./navigation.md#a-slidecontainer-in-the-url-a-position-that-is-not-a-place-one-came-from) |
117+
| a popup being open | `signal`, `open`, `navState` | [popup_open.md](./popup_open.md) |
118+
| where the user is | route + search-param signals | [navigation.md](./navigation.md) |
119+
| a value proposed to a control | `command="--navi-update"` | [control_value.md](./control_value.md#a-button-that-proposes-a-value-is---navi-update) |
120+
| the state of an async run | an `action`, read by `useAsyncData` | [actions.md](./actions.md) |
121121

122122
## When there is no binding
123123

packages/frontend/navi/src/layout/slide_container.jsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,6 @@ const durationToMs = (duration) => {
455455
// "somewhere over there" (a gesture one browses with), or it said a name (a tab
456456
// pressed, a key, a command — a place aimed at). Nothing at all when the travel
457457
// came from code, which has no interaction to speak of.
458-
const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
459-
460458
const causeOfEvent = (event) => {
461459
if (!event) {
462460
return "code";
@@ -471,6 +469,10 @@ const causeOfEvent = (event) => {
471469
return "command";
472470
};
473471

472+
// The causes that are not places one went, whatever the state they are written
473+
// into says (see writeAreaAsked).
474+
const CAUSES_WRITTEN_BY_REPLACEMENT = ["drag", "refusal", "state"];
475+
474476
const readArea = (slideElement) =>
475477
slideElement.getAttribute("data-slide-area") || slideElement.id || "";
476478

@@ -525,16 +527,34 @@ const readArea = (slideElement) =>
525527
* still unanswered) is where one stops — after which the signal is written
526528
* with the area actually shown, so it says where one IS and never where one
527529
* asked to be.
528-
* Hand it a `stateSignal` declared on a route (`searchParams: { step }`) and
529-
* the address is that state: `?step=<area>` on every travel, and the walk
530-
* above is what a link, a bookmark or a traversal goes through. Written by
530+
* PUTTING THE AREA IN THE URL is that binding and nothing more: hand it a
531+
* `stateSignal` a route declares as a search param, and `?step=<area>` is
532+
* written on every travel, read on a load, a bookmark, a link, a traversal —
533+
* through the walk above, so an address cannot open a slide the walk may not
534+
* reach.
535+
* ```js
536+
* const stepSignal = stateSignal("when", { id: "step", weak: true });
537+
* route("/alerts/:id/edit", { searchParams: { step: stepSignal } });
538+
* ```
539+
* ```jsx
540+
* <SlideContainer signal={stepSignal}>
541+
* ```
542+
* Where it opens is the state's own default (`stateSignal`'s first argument —
543+
* a signal there, for a start that depends on where one is), and the param
544+
* stays out of the address while the area IS that default. `weak` keeps the
545+
* step from being inherited by links built to that route. Written by
531546
* replacement unless the state says `history: "push"`, and even then a slide
532547
* reached by DRAGGING replaces — see docs/navigation.md.
533-
* @param {string} [props.defaultCurrent] - which slide to open on, when the
534-
* travel is left to the container. Mount-only, like every other `default*`:
548+
* @param {string} [props.defaultCurrent] - which slide to open on, for a
549+
* container that owns its position. Mount-only, like every other `default*`:
535550
* it says where one starts, not where one is — say `current` for that.
536551
* Without it the first slide is the one shown, the way a stack of pages opens
537552
* on its first page.
553+
* A container bound to a `signal` does not own its position, and where it
554+
* opens is that state's own default (`stateSignal`'s first argument) rather
555+
* than this: one place says where the area starts, and it is the place a
556+
* reset goes back to. This still answers for a signal holding nothing at
557+
* all.
538558
* @param {(area: string, detail: {cause: "drag"|"keyboard"|"command"|"code"|"state", event: Event}) => void|false|Promise<void|false>} [props.onCurrentChange]
539559
* - the slide being shown has changed. `cause` says what asked for it, which
540560
* is what tells a place browsed past from a place aimed at — a slide dragged

packages/frontend/navi/src/nav/route.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ let isUpdatingRoutesFromUrl = false;
2929
* `:name=${signal}` binds that param to a signal.
3030
* @param {object} [options]
3131
* @param {Object<string, import("@preact/signals").Signal>} [options.searchParams]
32-
* Search params this route two-way syncs with, by name.
32+
* Search params this route two-way syncs with, by name. The signal and the url
33+
* are the same state: writing the signal rewrites the address, an address
34+
* arriving from outside writes the signal, and the param disappears from the
35+
* url while the signal holds its default.
36+
* Writing one AMENDS the history entry one is on — a param qualifies the
37+
* screen, it is not a place, and one entry per write turns a single
38+
* back-press into as many as the user moved. A state whose values ARE places
39+
* says so where it is declared (`stateSignal(v, { history: "push" })`), and
40+
* one write can still say otherwise (`signal.set(v, { history })`).
3341
* @param {Object<string, RegExp | Array | ((value: string) => boolean)>} [options.params]
3442
* Which values a path param accepts, by param name: a regexp tested against the
3543
* decoded segment, the list of accepted values (compared as strings, so it can

packages/frontend/navi/src/text/demos/text_overflow_demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ <h2 id="s-text-align">Text Align</h2>
410410
style={{ width: "200px" }}
411411
/>
412412
</label>
413-
<Box row spacing="16px" style={{ flexWrap: "wrap" }}>
413+
<Box flex="y" spacing="16px">
414414
{["Marie D.", "Marie-Christine D."].map((name) => (
415415
<Box
416416
key={name}

0 commit comments

Comments
 (0)