@jsenv/navi 0.29.94 → 0.29.96

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.
@@ -177,18 +177,67 @@ it. It matters most inside a picker's popup, where nothing else would bring the
177
177
  value back down: a picker's value IS what the control in its popup holds, so the
178
178
  façade never echoes it back.
179
179
 
180
- A group with an `aggregateChildStates` of its own is outside all of this: what
181
- it returns is taken as the truth, `undefined` included. That is the one way such
182
- a group says **"I have nothing to say yet"** — return `undefined` while the
183
- children it needs are missing, and navi leaves the value alone instead of
184
- publishing an empty shape upward. A group written to always return its keys
185
- (`{ mode: undefined, levels: [] }`) is claiming an answer nobody gave, and that
186
- claim overwrites the row above it the moment the popup opens.
187
-
188
- Together they are what makes the two-hop case work — the form fills the row, the
189
- row fills the control in its popup whatever order the pieces turn up in. Get
190
- one of them wrong and the symptom is always the same: a value that was there
191
- before the popup opened, and empty after.
180
+ Four things follow, and each of them was paid for twice before being written
181
+ here:
182
+
183
+ - **the group's value has to be the view that carries the MOST.** Four seats say
184
+ who plays AND who sits where; a list of who plays says half of that. Make the
185
+ seating the value and the list is derived from it (`value.filter(Boolean)`);
186
+ make the list the value and every gesture on a seat has to be guessed at. The
187
+ rule to apply: between two views of one answer, the value is the one the other
188
+ can be computed from;
189
+ - **the Map must name EVERY child**, the derived view included. A child the Map
190
+ does not name is left where it is which reads as "the list never fills";
191
+ - **name the children you WANT, never exclude the one you don't.** A group holds
192
+ more than the views: a search box, a filter, whatever the popup needs. Reading
193
+ the seats as "every child except the list" collects those too, and the value
194
+ ends up with five slots for four seats. Finding them by name says what it
195
+ means;
196
+ - **a gesture that moves two children must never leave the value in an
197
+ in-between.** Dragging somebody from one seat to another is one intention and
198
+ two writes, and the group aggregates between them: for one instant the person
199
+ is in the list and in no seat, which a value that IS the seating has no room
200
+ for. Measured, that instant costs the person — the aggregate drops them and
201
+ the distribute unticks them before the second write lands. Two ways out, and
202
+ the first is the one to prefer:
203
+
204
+ 1. **write the group's value once.** `rowSignal.value = slots` with both seats
205
+ already moved: there is no in-between at all. Available when the row is
206
+ where the placement lives;
207
+ 2. **write the destination first**, when the seats are the truth and must stay
208
+ separate signals. The person is somewhere at every observable moment (in
209
+ two seats for an instant, which the rule then resolves), instead of nowhere.
210
+ Source-first loses them — measured, and silently.
211
+
212
+ Wrapping the two writes in a signal `batch()` does NOT help: the group
213
+ aggregates on each child's change, not on the render that follows.
214
+
215
+ ```jsx
216
+ // the seats are found by name, never as "everything that is not the list"
217
+ const seatChildren = (children) =>
218
+ SEAT_NAMES.map((name) => children.find((child) => child.name === name));
219
+
220
+ // the value IS the seating; the list is a view of it
221
+ <ControlGroup
222
+ aggregateChildStates={(children) => {…}} // seats, minus who the list dropped, plus who it added
223
+ distributeChildStates={(slots, children) =>
224
+ new Map([
225
+ ...seats.map((seat, i) => [seat, slots[i]]),
226
+ [list, slots.filter(Boolean)],
227
+ ])
228
+ }
229
+ >
230
+ ```
231
+
232
+ ```js
233
+ // and a drag is one write
234
+ const moveTo = (from, to) => {
235
+ const slots = [...rowSignal.value];
236
+ slots[to] = slots[from];
237
+ slots[from] = undefined;
238
+ rowSignal.value = slots;
239
+ };
240
+ ```
192
241
 
193
242
  ## One line, one key
194
243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.94",
3
+ "version": "0.29.96",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {