@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.
@@ -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
@@ -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
- ### The build target has to match the css
221
-
222
- The css is transpiled for the runtimes the build targets, so a target older than
223
- what the css uses is silently lowered. `light-dark()` under a target below Chrome
224
- 123 becomes `var(--lightningcss-light, …) var(--lightningcss-dark, …)`, an
225
- invalid declaration — the color is simply not applied. navi's own build targets
226
- Chrome 123 because navi's css uses `light-dark()`; an app targeting older
227
- browsers must not use css newer than its target.
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
 
@@ -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
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.145",
3
+ "version": "0.29.147",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {