@jsenv/navi 0.29.93 → 0.29.95

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.
@@ -146,8 +146,9 @@ consistency across the app, not from any single call site.
146
146
  `canSendWhileUnchanged` because a submit "does nothing".
147
147
  - `distributeChildStates` on a group — the way down asked once for ALL the
148
148
  children, when they cannot be placed one at a time (four seats where who sits
149
- down decides who moves). The mirror of `aggregateChildStates`; see
150
- control_object.md. Read it before writing a `uiAction` whose only job is to
149
+ down decides who moves), and asked again whenever one of them speaks, so the
150
+ other views of the same answer follow. The mirror of `aggregateChildStates`;
151
+ see control_object.md. Read it before writing a `uiAction` whose only job is to
151
152
  translate. Two neighbouring questions have answers already: a yes/no shown as
152
153
  two rows needs no translation (`List.Item value={true}` beside
153
154
  `value={false}` IS a boolean control), and a cross that means "back to the
@@ -164,18 +164,60 @@ distributeChildStates: (groupValue, children) => new Map([[child, state], …])
164
164
  the mirror of `aggregateChildStates`, which already sees every child. A child
165
165
  the Map does not name is left where it is. Given both, the plural one wins.
166
166
 
167
- A group with an `aggregateChildStates` of its own is outside all of this: what
168
- it returns is taken as the truth, `undefined` included. That is the one way such
169
- a group says **"I have nothing to say yet"** return `undefined` while the
170
- children it needs are missing, and navi leaves the value alone instead of
171
- publishing an empty shape upward. A group written to always return its keys
172
- (`{ mode: undefined, levels: [] }`) is claiming an answer nobody gave, and that
173
- claim overwrites the row above it the moment the popup opens.
174
-
175
- Together they are what makes the two-hop case work the form fills the row, the
176
- row fills the control in its popup whatever order the pieces turn up in. Get
177
- one of them wrong and the symptom is always the same: a value that was there
178
- before the popup opened, and empty after.
167
+ **It closes the loop, which the per-child one does not.** A group with a plural
168
+ distribute holds ONE answer its children are views OF a list saying who plays,
169
+ four seats saying who sits where rather than a value that IS what they said.
170
+ So when one view speaks, the answer moves and **the other views are placed
171
+ again**; the one that just acted is left where the user put it. Without that, a
172
+ seat keeps showing somebody the list no longer holds, and nothing says so.
173
+
174
+ This is the same rule as "a child arriving after the value did is placed from
175
+ what the group holds", for a child that was there when the value moved without
176
+ it. It matters most inside a picker's popup, where nothing else would bring the
177
+ value back down: a picker's value IS what the control in its popup holds, so the
178
+ façade never echoes it back.
179
+
180
+ Three things follow, and all three were 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
+ - **a gesture that moves two children writes the GROUP's value, not the two
192
+ children.** Dragging somebody from one seat to another is one intention and
193
+ two writes; written one after the other, the group re-places between them, the
194
+ rule sees a free seat and puts the person back where they were. Written as one
195
+ change of the group's value there is nothing in between. (Wrapping the two
196
+ writes in a signal `batch()` does NOT do it: the group aggregates on each
197
+ child's change, not on the render that follows.)
198
+
199
+ ```jsx
200
+ // the value IS the seating; the list is a view of it
201
+ <ControlGroup
202
+ aggregateChildStates={(children) => {…}} // seats, minus who the list dropped, plus who it added
203
+ distributeChildStates={(slots, children) =>
204
+ new Map([
205
+ ...seats.map((seat, i) => [seat, slots[i]]),
206
+ [list, slots.filter(Boolean)],
207
+ ])
208
+ }
209
+ >
210
+ ```
211
+
212
+ ```js
213
+ // and a drag is one write
214
+ const moveTo = (from, to) => {
215
+ const slots = [...rowSignal.value];
216
+ slots[to] = slots[from];
217
+ slots[from] = undefined;
218
+ rowSignal.value = slots;
219
+ };
220
+ ```
179
221
 
180
222
  ## One line, one key
181
223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.29.93",
3
+ "version": "0.29.95",
4
4
  "type": "module",
5
5
  "description": "Library of components including navigation to create frontend applications",
6
6
  "repository": {