@jsenv/navi 0.29.145 → 0.29.147
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 +129 -112
- package/dist/jsenv_navi.js.map +18 -11
- package/dist/jsenv_navi_side_effects.js +98 -0
- package/dist/jsenv_navi_side_effects.js.map +3 -1
- package/docs/AI_INSTRUCTIONS.md +16 -7
- package/docs/actions.md +4 -0
- package/docs/autofocus.md +68 -2
- package/docs/control_value.md +32 -0
- package/docs/css_architecture.md +27 -8
- package/docs/popup_open.md +18 -0
- package/docs/state_binding.md +34 -0
- package/package.json +1 -1
package/docs/control_value.md
CHANGED
|
@@ -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 picker fills its popup: the control inside already knows](#a-picker-fills-its-popup-the-control-inside-already-knows)
|
|
9
10
|
- [A button that proposes a value is `--navi-update`](#a-button-that-proposes-a-value-is---navi-update)
|
|
10
11
|
- [`--navi-update:smooth`: the control is seen answering](#--navi-updatesmooth-the-control-is-seen-answering)
|
|
11
12
|
- [`signal` + `defaultValue`: the answer and where it starts](#signal--defaultvalue-the-answer-and-where-it-starts)
|
|
@@ -74,6 +75,37 @@ A value pushed in from anywhere is an answer like any other: the wheels roll,
|
|
|
74
75
|
and the submit lights up. Which is why a **button** offering such a value is not
|
|
75
76
|
a hand-written signal write — see the next section.
|
|
76
77
|
|
|
78
|
+
## A picker fills its popup: the control inside already knows
|
|
79
|
+
|
|
80
|
+
A picker mirrors the control in its popup, both ways
|
|
81
|
+
([popup_open.md](./popup_open.md#composing-a-value-or-doing-work)) — and the
|
|
82
|
+
first half of that mirroring happens **at open**: the picker fills the control
|
|
83
|
+
with the value it holds, before anything is shown.
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
// the row matching the picker's value is already selected, painted and
|
|
87
|
+
// announced — nothing here says which one it is
|
|
88
|
+
<Picker id="channel" signal={channelSignal}>
|
|
89
|
+
<List selectable>
|
|
90
|
+
{CHANNELS.map((channel) => (
|
|
91
|
+
<List.Item key={channel.id} value={channel.id}>
|
|
92
|
+
{channel.label}
|
|
93
|
+
</List.Item>
|
|
94
|
+
))}
|
|
95
|
+
</List>
|
|
96
|
+
</Picker>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
So a `current`, a `selected` per row, or a `value` computed from what the picker
|
|
100
|
+
holds is not "being explicit": it is a second answer to a question already
|
|
101
|
+
answered, and it is the one that goes stale — the picker's value moves on a
|
|
102
|
+
cancel, on a `--navi-update`, on a signal written elsewhere, and none of those
|
|
103
|
+
pass through the prop the app is computing.
|
|
104
|
+
|
|
105
|
+
What is left for the app is what to make of the selection, not what it is:
|
|
106
|
+
a row is marked (`aria-selected`) and painted by the list itself, so anything
|
|
107
|
+
extra hangs off that marker in CSS rather than off a prop.
|
|
108
|
+
|
|
77
109
|
## A button that proposes a value is `--navi-update`
|
|
78
110
|
|
|
79
111
|
A shortcut beside a control — "Tous niveaux" / "Aucun niveau" next to a list of
|
package/docs/css_architecture.md
CHANGED
|
@@ -217,14 +217,33 @@ before splitting. In navi, `popup_css.js` and `surface_text_css.js` overlap
|
|
|
217
217
|
`.navi_dialog` and `.navi_popover` with dialog's and popover's own css, which is
|
|
218
218
|
why they are still concatenated.
|
|
219
219
|
|
|
220
|
-
###
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
### Browser support: navi's css and your target
|
|
221
|
+
|
|
222
|
+
**navi does not require recent browsers.** Its css is written with modern
|
|
223
|
+
features — css nesting, `light-dark()`, `color-mix()` — and it ships that way:
|
|
224
|
+
navi's own build targets a runtime where all of it is native, so what lands in
|
|
225
|
+
`dist/` is what was authored, unrewritten.
|
|
226
|
+
|
|
227
|
+
The runtime that decides is **yours**. An app's build reads navi's stylesheets
|
|
228
|
+
out of `node_modules` like any other css and lowers them to the target the app
|
|
229
|
+
declares. Targeting Chrome 89, everything above comes out as nesting-free rules
|
|
230
|
+
and a pair of custom properties standing in for `light-dark()`; targeting the
|
|
231
|
+
default, nothing is touched.
|
|
232
|
+
|
|
233
|
+
Which is the part worth knowing: **jsenv's default target is recent**, and by
|
|
234
|
+
default it assumes the modern form is fine. An app that must support older
|
|
235
|
+
browsers says so, once, and both its dev server and its build follow — the
|
|
236
|
+
declaration and what each step down costs are in
|
|
237
|
+
[jsenv's browser support](https://github.com/jsenv/core/blob/main/docs/users/c_build/c_build.md#21-browser-support).
|
|
238
|
+
Nothing in the app's own css has to change either: it is lowered by the same
|
|
239
|
+
pass.
|
|
240
|
+
|
|
241
|
+
One thing is NOT lowered, and it is the trap the section above already names: a
|
|
242
|
+
stylesheet the build cannot parse. A `${}` outside a value position makes the
|
|
243
|
+
whole template opaque, and an opaque template is shipped verbatim — nesting
|
|
244
|
+
included, `light-dark()` included, whatever the target. Such a stylesheet
|
|
245
|
+
silently requires the browsers its own css requires, and no declaration can
|
|
246
|
+
lower it. That is the strongest reason not to write one.
|
|
228
247
|
|
|
229
248
|
---
|
|
230
249
|
|
package/docs/popup_open.md
CHANGED
|
@@ -552,6 +552,24 @@ const GroupMembers = () => {
|
|
|
552
552
|
};
|
|
553
553
|
```
|
|
554
554
|
|
|
555
|
+
**A popup that waits holds its own `<Loading>`**, like every other part of a
|
|
556
|
+
screen that can wait — it is not built into `Dialog`/`Popover`, because only the
|
|
557
|
+
code inside knows whether anything in there loads at all:
|
|
558
|
+
|
|
559
|
+
```jsx
|
|
560
|
+
<Dialog signal={groupSheetOpenSignal}>
|
|
561
|
+
<Loading fallback={<GroupMembersSkeleton />}>
|
|
562
|
+
<GroupMembers />
|
|
563
|
+
</Loading>
|
|
564
|
+
</Dialog>
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Without it the nearest boundary is the one at the root of the app, and a
|
|
568
|
+
suspension there empties the whole page — taking with it the popup element the
|
|
569
|
+
browser is in the middle of animating. The alternative is the component drawing
|
|
570
|
+
its own wait (`useAsyncData(action, { loading: true })`), which needs no
|
|
571
|
+
boundary; what is not an option is neither of the two.
|
|
572
|
+
|
|
555
573
|
What the popup gains is everything a page has:
|
|
556
574
|
|
|
557
575
|
- the request leaves **with the screen**, in parallel with the rest of what the
|
package/docs/state_binding.md
CHANGED
|
@@ -11,6 +11,7 @@ then on there are two owners for one truth.
|
|
|
11
11
|
- [Why it is not merely shorter](#why-it-is-not-merely-shorter)
|
|
12
12
|
- [What is left for the callback](#what-is-left-for-the-callback)
|
|
13
13
|
- [What binds itself](#what-binds-itself)
|
|
14
|
+
- [A control that shows part of a bigger answer](#a-control-that-shows-part-of-a-bigger-answer)
|
|
14
15
|
- [When there is no binding](#when-there-is-no-binding)
|
|
15
16
|
|
|
16
17
|
## The three shapes
|
|
@@ -119,6 +120,39 @@ Binding the state does not remove the callback — it removes one job from it:
|
|
|
119
120
|
| 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
121
|
| the state of an async run | an `action`, read by `useAsyncData` | [actions.md](./actions.md) |
|
|
121
122
|
|
|
123
|
+
## A control that shows part of a bigger answer
|
|
124
|
+
|
|
125
|
+
"Who writes the score" has three answers — a player of the game, the organizer
|
|
126
|
+
alone, nobody — and only two of them are rows: "nobody" is a switch under the
|
|
127
|
+
list. So the state holds a value the list cannot show, and binding the list to
|
|
128
|
+
that state leaves it showing nothing whenever the switch is on.
|
|
129
|
+
|
|
130
|
+
The reflex is to fall back on `value` + `uiAction` and remember the row by hand.
|
|
131
|
+
The finding is elsewhere: **the list is not asking that question**. It asks
|
|
132
|
+
"which of these two", the switch asks "anybody at all", and the answer the
|
|
133
|
+
screen sends is made of both:
|
|
134
|
+
|
|
135
|
+
```jsx
|
|
136
|
+
const scorerSignal = signal("player"); // what the list asks: one of ITS rows
|
|
137
|
+
const nobodySignal = signal(false); // what the switch asks
|
|
138
|
+
// what the screen answers, made of the two
|
|
139
|
+
const whoWritesSignal = computed(() =>
|
|
140
|
+
nobodySignal.value ? "nobody" : scorerSignal.value,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
<List selectable signal={scorerSignal}>…</List>
|
|
144
|
+
<Input type="checkbox" signal={nobodySignal} />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Each control binds the state it is about, and the value that spans them is
|
|
148
|
+
computed from those — never held a second time. It also gives back for free what
|
|
149
|
+
the hand-written version had to arrange: the row chosen before the switch went
|
|
150
|
+
on is still chosen when it goes off, because nothing ever cleared it.
|
|
151
|
+
|
|
152
|
+
The rule underneath: a control shows values, so the state it binds is the one
|
|
153
|
+
whose values it can show. A state holding more than that is more than one
|
|
154
|
+
question.
|
|
155
|
+
|
|
122
156
|
## When there is no binding
|
|
123
157
|
|
|
124
158
|
Sometimes the state being wired by hand has no prop for it. That is not a
|