@jsenv/navi 0.29.389 → 0.29.390

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.
@@ -4955,14 +4955,11 @@ const CONTROL_TYPE_BY_SIGNAL_TYPE = {
4955
4955
  // become something plainer — it is what says the value is a number (see
4956
4956
  // isNumberInput), and what lets constraint messages use domain-specific
4957
4957
  // wording instead of the generic "Ce nombre doit être...".
4958
+ // A type a picker preset draws (navi_time, navi_minute: see picker_preset.jsx)
4959
+ // has no row: resolveInputProps runs before the preset resolver, and a row
4960
+ // would turn the type into its host before the preset resolver reads it. The
4961
+ // preset hands the host type on itself.
4958
4962
  const NAVI_TYPE_DEFAULTS = {
4959
- navi_time: {
4960
- "type": "time",
4961
- "navi-input-type": "time",
4962
- "min": 0,
4963
- "max": 24 * 3600 - 1,
4964
- "step": 1,
4965
- },
4966
4963
  navi_percentage: {
4967
4964
  "type": "navi_number",
4968
4965
  "navi-input-type": "percentage",
@@ -5024,7 +5021,6 @@ const isNumberInput = (type, naviInputType) => {
5024
5021
  * Supported navi types and their targets:
5025
5022
  * - `navi_percentage` → `navi_number` (0–100, step 1)
5026
5023
  * - `navi_number` → `text` (inputMode="numeric", no spin buttons implied)
5027
- * - `navi_time` → `time` (step in seconds)
5028
5024
  *
5029
5025
  * Standard HTML input types with formatters:
5030
5026
  * - `date`, `month`, `week`, `time`, `datetime-local`, `datetime`:
@@ -16611,6 +16607,15 @@ const createResource = (
16611
16607
  paramScope,
16612
16608
  rerunOn,
16613
16609
  dependencies,
16610
+ // Set on a scoped child: the resource owning each of its items, which
16611
+ // live in one store per owner (`store` is the owner's).
16612
+ ownerName,
16613
+ // Set on a scopedMany child: its per-owner store looked up by owner key,
16614
+ // and the owner whose GET embeds the collection. Options rather than
16615
+ // properties added afterwards, so every facade of the relation — the one
16616
+ // scopedMany returns and each withParams() of it — carries both.
16617
+ getChildStore,
16618
+ ownerDependency,
16614
16619
  },
16615
16620
  ) => {
16616
16621
  const params = paramScope.params;
@@ -16636,6 +16641,8 @@ const createResource = (
16636
16641
  // private but exposed for convenience
16637
16642
  store,
16638
16643
  addItemSetup,
16644
+ ownerName,
16645
+ getChildStore,
16639
16646
  };
16640
16647
 
16641
16648
  resourceLifecycleManager.registerResource(stateFacade, {
@@ -16644,6 +16651,17 @@ const createResource = (
16644
16651
  dependencies,
16645
16652
  uniqueKeys,
16646
16653
  });
16654
+ if (ownerDependency) {
16655
+ // A child collection mutated (POST etc.) makes the owner GET re-fetch:
16656
+ // the owner embeds the child array and only the backend knows the new
16657
+ // ordering. Registered per facade, since the rerun is looked up by the
16658
+ // facade whose action completed.
16659
+ resourceLifecycleManager.addDependency(
16660
+ stateFacade,
16661
+ ownerDependency.owner,
16662
+ ownerDependency.propertyName,
16663
+ );
16664
+ }
16647
16665
  const onActionComplete = (actionCompleted) => {
16648
16666
  resourceLifecycleManager.onActionComplete(actionCompleted, {
16649
16667
  resourceScope: stateFacade,
@@ -16656,6 +16674,10 @@ const createResource = (
16656
16674
  * Actions from parameterized resources only trigger rerun/reset for other actions with
16657
16675
  * identical parameters, preventing cross-contamination between different parameter sets.
16658
16676
  *
16677
+ * The actions are the resource's own kind: on a relation they keep its callback return
16678
+ * contract and write where it writes (`TABLE_COLUMNS.withParams(…).GET_MANY` returns
16679
+ * `[ownerId, items]` and replaces that owner's collection).
16680
+ *
16659
16681
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16660
16682
  * @param {{ rerunOn?: Object, dependencies?: Object[] }} [options] - reruns of that scope; a `rerunOn` key left out keeps the resource's value, and `dependencies` left out are the resource's
16661
16683
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
@@ -16681,7 +16703,6 @@ const createResource = (
16681
16703
  paramsToInject,
16682
16704
  { dependencies: withParamsDeps, rerunOn: withParamsRerunOn } = {},
16683
16705
  ) => {
16684
- const declarationSite = getDeclarationSite();
16685
16706
  if (!paramsToInject || Object.keys(paramsToInject).length === 0) {
16686
16707
  throw new Error(`resource(${name}).withParams() requires parameters`);
16687
16708
  }
@@ -16689,11 +16710,6 @@ const createResource = (
16689
16710
  ? { ...params, ...paramsToInject }
16690
16711
  : paramsToInject;
16691
16712
  const resolvedParamScope = getParamScope(resolvedParams);
16692
- const createRestActionWithParams = createRestActionFactoryForRoot(name, {
16693
- idKey,
16694
- store,
16695
- declarationSite,
16696
- });
16697
16713
  return createResource(name, {
16698
16714
  idKey,
16699
16715
  uniqueKeys,
@@ -16701,10 +16717,16 @@ const createResource = (
16701
16717
  restCallbacks,
16702
16718
  store,
16703
16719
  addItemSetup,
16704
- createRestAction: createRestActionWithParams,
16720
+ // The actions of this resource's kind: a relation applies its results to
16721
+ // its relation (a scoped child to the per-owner stores), never as root
16722
+ // items of `store`.
16723
+ createRestAction,
16705
16724
  paramScope: resolvedParamScope,
16706
16725
  rerunOn: resolveRerunOn(withParamsRerunOn, rerunOn),
16707
16726
  dependencies: withParamsDeps ?? dependencies,
16727
+ ownerName,
16728
+ getChildStore,
16729
+ ownerDependency,
16708
16730
  });
16709
16731
  };
16710
16732
  stateFacade.withParams = withParams;
@@ -16749,6 +16771,13 @@ const createResource = (
16749
16771
  } = {},
16750
16772
  ) => {
16751
16773
  const declarationSite = getDeclarationSite();
16774
+ if (ownerName) {
16775
+ refuseCallbacksNamingScopedItem(
16776
+ `${name}.one("${propertyName}")`,
16777
+ { GET, PUT, DELETE },
16778
+ { name, ownerName, propertyName, declarationSite },
16779
+ );
16780
+ }
16752
16781
  const childName = `${name}.${propertyName}`;
16753
16782
  const childIdKey = childResource.idKey;
16754
16783
  const childStore = childResource.store;
@@ -16809,7 +16838,11 @@ const createResource = (
16809
16838
  );
16810
16839
  });
16811
16840
 
16812
- const createRestActionForOne = (verb, callback, { onActionComplete }) => {
16841
+ const createRestActionForOne = (
16842
+ verb,
16843
+ callback,
16844
+ { onActionComplete, paramScope },
16845
+ ) => {
16813
16846
  const applyResultToValue =
16814
16847
  verb === "DELETE"
16815
16848
  ? (itemIdOrItemProps) => {
@@ -16924,6 +16957,13 @@ const createResource = (
16924
16957
  } = {},
16925
16958
  ) => {
16926
16959
  const declarationSite = getDeclarationSite();
16960
+ if (ownerName) {
16961
+ refuseCallbacksNamingScopedItem(
16962
+ `${name}.many("${propertyName}")`,
16963
+ { GET_MANY, DELETE, DELETE_MANY },
16964
+ { name, ownerName, propertyName, declarationSite },
16965
+ );
16966
+ }
16927
16967
  const childStore = childResource.store;
16928
16968
  const childIdKey = childResource.idKey;
16929
16969
  const childName = `${name}.${propertyName}`;
@@ -16955,21 +16995,23 @@ const createResource = (
16955
16995
  const createRestActionForMany = (
16956
16996
  verb,
16957
16997
  callback,
16958
- { isMany, onActionComplete },
16998
+ { isMany, onActionComplete, paramScope },
16959
16999
  ) => {
16960
17000
  if (!isMany) {
16961
17001
  return createRestActionAffectingOneItem(verb, callback, {
16962
17002
  onActionComplete,
17003
+ paramScope,
16963
17004
  });
16964
17005
  }
16965
17006
  return createRestActionAffectingManyItems(verb, callback, {
16966
17007
  onActionComplete,
17008
+ paramScope,
16967
17009
  });
16968
17010
  };
16969
17011
  const createRestActionAffectingOneItem = (
16970
17012
  verb,
16971
17013
  callback,
16972
- { onActionComplete },
17014
+ { onActionComplete, paramScope },
16973
17015
  ) => {
16974
17016
  const applyResultToValue =
16975
17017
  verb === "DELETE"
@@ -17032,7 +17074,7 @@ const createResource = (
17032
17074
  const createRestActionAffectingManyItems = (
17033
17075
  verb,
17034
17076
  callback,
17035
- { onActionComplete },
17077
+ { onActionComplete, paramScope },
17036
17078
  ) => {
17037
17079
  const applyResultToValue =
17038
17080
  verb === "GET"
@@ -17160,7 +17202,12 @@ const createResource = (
17160
17202
  * Mutations apply directly to the owner's signal, so the parent GET is never rerun.
17161
17203
  *
17162
17204
  * Returns the child relationship resource, itself chainable:
17163
- * `USER_PROFILE.one("theme", THEME)` adds a reactive `.theme` property on each profile.
17205
+ * `USER_PROFILE.one("theme", THEME)` adds a reactive `.theme` property on each profile,
17206
+ * fed by what this resource's callbacks embed (`[id, { bio, theme: { id: 3 } }]`). A
17207
+ * profile exists only inside its user, so the chained relation declares none of the
17208
+ * callbacks whose result names a profile (`.one()` GET/PUT/DELETE, `.many()`
17209
+ * GET_MANY/DELETE/DELETE_MANY, any `.scopedOne()`/`.scopedMany()` callback): such a
17210
+ * result cannot say which user holds it, and the declaration throws.
17164
17211
  *
17165
17212
  * @param {string} propertyName - property holding the sub-object on each owner item
17166
17213
  * @param {Object} [restCallbacks] - `{ idKey, rerunOn, dependencies, GET, POST, PUT, PATCH, DELETE }`
@@ -17180,6 +17227,14 @@ const createResource = (
17180
17227
  DELETE,
17181
17228
  } = {},
17182
17229
  ) => {
17230
+ const declarationSite = getDeclarationSite();
17231
+ if (ownerName) {
17232
+ refuseCallbacksNamingScopedItem(
17233
+ `${name}.scopedOne("${propertyName}")`,
17234
+ { GET, POST, PUT, PATCH, DELETE },
17235
+ { name, ownerName, propertyName, declarationSite },
17236
+ );
17237
+ }
17183
17238
  const childName = `${name}.${propertyName}`;
17184
17239
 
17185
17240
  // Callbacks added by chained .one()/.many() on the child resource,
@@ -17209,7 +17264,12 @@ const createResource = (
17209
17264
  childSignal.value = childItem; // first activation: null → childItem
17210
17265
  }
17211
17266
  };
17212
- applyPropsMap.set(ownerId, applyProps);
17267
+ if (!ownerName) {
17268
+ // Declared on a scoped child, an owner id is only unique inside its
17269
+ // own owner and no callback addresses it (see
17270
+ // refuseCallbacksNamingScopedItem): nothing is looked up by it.
17271
+ applyPropsMap.set(ownerId, applyProps);
17272
+ }
17213
17273
 
17214
17274
  applyProps(ownerItem[propertyName]);
17215
17275
 
@@ -17221,7 +17281,7 @@ const createResource = (
17221
17281
  const createRestActionForScopedOne = (
17222
17282
  verb,
17223
17283
  callback,
17224
- { onActionComplete },
17284
+ { onActionComplete, paramScope },
17225
17285
  ) => {
17226
17286
  const childActionName = `${childName}.${verb}`;
17227
17287
  return createAction(callback, {
@@ -17269,6 +17329,7 @@ const createResource = (
17269
17329
  paramScope,
17270
17330
  rerunOn: resolveRerunOn(scopedOneRerunOn, rerunOn),
17271
17331
  dependencies: scopedOneDependencies ?? dependencies,
17332
+ ownerName: name,
17272
17333
  });
17273
17334
  };
17274
17335
 
@@ -17296,7 +17357,14 @@ const createResource = (
17296
17357
  * `propertyName`), and the child's own GET_MANY reruns per its `rerunOn`.
17297
17358
  *
17298
17359
  * Returns the child relationship resource, itself chainable:
17299
- * `TABLE_COLUMNS.one("dataType", DATA_TYPE)` adds a reactive `.dataType` property on each column.
17360
+ * `TABLE_COLUMNS.one("dataType", DATA_TYPE)` adds a reactive `.dataType` property on each column,
17361
+ * fed by what this resource's callbacks embed (`[id, [{ name, dataType: { id: 2 } }]]`). A
17362
+ * column exists only inside its table — two tables can each have an `email` — so the chained
17363
+ * relation declares none of the callbacks whose result names a column (`.one()`
17364
+ * GET/PUT/DELETE, `.many()` GET_MANY/DELETE/DELETE_MANY, any `.scopedOne()`/`.scopedMany()`
17365
+ * callback): such a result cannot say which table holds it, and the declaration throws.
17366
+ * A `.scopedMany()` chained on it keeps one collection per column: `users.email` and
17367
+ * `admins.email` never share one.
17300
17368
  *
17301
17369
  * @param {string} propertyName - property holding the collection on each owner item
17302
17370
  * @param {Object} [restCallbacks] - `{ idKey, rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
@@ -17321,6 +17389,25 @@ const createResource = (
17321
17389
  DELETE_MANY,
17322
17390
  } = {},
17323
17391
  ) => {
17392
+ const declarationSite = getDeclarationSite();
17393
+ if (ownerName) {
17394
+ refuseCallbacksNamingScopedItem(
17395
+ `${name}.scopedMany("${propertyName}")`,
17396
+ {
17397
+ GET,
17398
+ GET_MANY,
17399
+ POST,
17400
+ POST_MANY,
17401
+ PUT,
17402
+ PUT_MANY,
17403
+ PATCH,
17404
+ PATCH_MANY,
17405
+ DELETE,
17406
+ DELETE_MANY,
17407
+ },
17408
+ { name, ownerName, propertyName, declarationSite },
17409
+ );
17410
+ }
17324
17411
  const childName = `${name}.${propertyName}`;
17325
17412
 
17326
17413
  // Callbacks added by chained .one()/.many() on the child resource,
@@ -17342,37 +17429,42 @@ const createResource = (
17342
17429
  return childItem;
17343
17430
  },
17344
17431
  });
17345
- const scope = { childStore, idArraySignal: signal([]) };
17346
- scopeMap.set(ownerKey, scope);
17347
- return scope;
17432
+ return { childStore, idArraySignal: signal([]) };
17348
17433
  };
17349
17434
  addItemSetup((item) => {
17350
17435
  const ownerId = item[idKey];
17351
-
17352
- // Reuse an existing scope if one was already created under a uniqueKey
17353
- // value (e.g. rows were fetched by tablename before the full table was loaded).
17354
- let scope = scopeMap.get(ownerId);
17355
- if (!scope) {
17436
+ let scope;
17437
+ if (ownerName) {
17438
+ // The owner is itself a scoped child: its id is only unique inside its
17439
+ // own owner, so the scope is the item's alone, never looked up by id
17440
+ // (no callback addresses it, see refuseCallbacksNamingScopedItem).
17441
+ scope = createScope(ownerId);
17442
+ } else {
17443
+ // Reuse an existing scope if one was already created under a uniqueKey
17444
+ // value (e.g. rows were fetched by tablename before the full table was loaded).
17445
+ scope = scopeMap.get(ownerId);
17446
+ if (!scope) {
17447
+ for (const uniqueKey of uniqueKeys) {
17448
+ const uniqueKeyValue = item[uniqueKey];
17449
+ if (uniqueKeyValue !== undefined && scopeMap.has(uniqueKeyValue)) {
17450
+ scope = scopeMap.get(uniqueKeyValue);
17451
+ break;
17452
+ }
17453
+ }
17454
+ }
17455
+ if (!scope) {
17456
+ scope = createScope(ownerId);
17457
+ }
17458
+ // Register the scope under the id and every uniqueKey value so that
17459
+ // resolveOwnerId can address it whichever key a callback returns.
17460
+ scopeMap.set(ownerId, scope);
17356
17461
  for (const uniqueKey of uniqueKeys) {
17357
17462
  const uniqueKeyValue = item[uniqueKey];
17358
- if (uniqueKeyValue !== undefined && scopeMap.has(uniqueKeyValue)) {
17359
- scope = scopeMap.get(uniqueKeyValue);
17360
- break;
17463
+ if (uniqueKeyValue !== undefined) {
17464
+ scopeMap.set(uniqueKeyValue, scope);
17361
17465
  }
17362
17466
  }
17363
17467
  }
17364
- if (!scope) {
17365
- scope = createScope(ownerId);
17366
- }
17367
- // Register the scope under the id and every uniqueKey value so that
17368
- // resolveOwnerId can address it whichever key a callback returns.
17369
- scopeMap.set(ownerId, scope);
17370
- for (const uniqueKey of uniqueKeys) {
17371
- const uniqueKeyValue = item[uniqueKey];
17372
- if (uniqueKeyValue !== undefined) {
17373
- scopeMap.set(uniqueKeyValue, scope);
17374
- }
17375
- }
17376
17468
 
17377
17469
  const { childStore, idArraySignal } = scope;
17378
17470
  const updateChildItemIdArray = createChildIdArrayUpdater(
@@ -17410,7 +17502,7 @@ const createResource = (
17410
17502
  const createRestActionForScopedMany = (
17411
17503
  verb,
17412
17504
  callback,
17413
- { isMany, onActionComplete },
17505
+ { isMany, onActionComplete, paramScope },
17414
17506
  ) => {
17415
17507
  const childActionName = `${childName}.${verb}`;
17416
17508
  return createAction(callback, {
@@ -17432,7 +17524,11 @@ const createResource = (
17432
17524
  );
17433
17525
  // Owner not in store yet: create the scope so actions can run before
17434
17526
  // the parent item has been loaded (e.g. rows fetched before their table).
17435
- const scope = scopeMap.get(ownerId) || createScope(ownerId);
17527
+ let scope = scopeMap.get(ownerId);
17528
+ if (!scope) {
17529
+ scope = createScope(ownerId);
17530
+ scopeMap.set(ownerId, scope);
17531
+ }
17436
17532
  const { childStore, idArraySignal } = scope;
17437
17533
 
17438
17534
  if (verb === "DELETE") {
@@ -17504,18 +17600,17 @@ const createResource = (
17504
17600
  paramScope,
17505
17601
  rerunOn: resolveRerunOn(scopedManyRerunOn, rerunOn),
17506
17602
  dependencies: scopedManyDependencies ?? dependencies,
17603
+ ownerName: name,
17604
+ // Declared on a scoped child, the scopes are kept per item and never
17605
+ // registered by id (see addItemSetup above): there is no owner key to
17606
+ // find one by.
17607
+ getChildStore: ownerName
17608
+ ? undefined
17609
+ : (ownerKey) => scopeMap.get(ownerKey)?.childStore,
17610
+ // scopedOne needs no such dependency: its mutation result carries the
17611
+ // updated object directly.
17612
+ ownerDependency: { owner: stateFacade, propertyName },
17507
17613
  });
17508
- // When a scoped child collection is mutated (POST etc.), the parent GET must
17509
- // re-fetch: the parent embeds the child array and only the backend knows the
17510
- // new ordering. (scopedOne does not need this: the mutation result contains
17511
- // the updated object directly.)
17512
- resourceLifecycleManager.addDependency(
17513
- childResource,
17514
- stateFacade,
17515
- propertyName,
17516
- );
17517
- childResource.getChildStore = (ownerKey) =>
17518
- scopeMap.get(ownerKey)?.childStore;
17519
17614
  return childResource;
17520
17615
  };
17521
17616
 
@@ -17736,7 +17831,7 @@ const getLastGetItemId = (action) => {
17736
17831
  };
17737
17832
 
17738
17833
  // Captures the "file:line:column" of the user code that invoked the public
17739
- // function (resource(), .one(), .many(), withParams, …), so invalid-result
17834
+ // function (resource(), .one(), .many(), .scopedOne(), …), so invalid-result
17740
17835
  // errors can point at where the callbacks were declared, not at this file.
17741
17836
  // Must be called directly from the public function: the stack offset accounts
17742
17837
  // for exactly two frames (getCallerInfo → getDeclarationSite → public fn → user code).
@@ -17757,6 +17852,36 @@ ${originalActionName} source location: ${declarationSite}`,
17757
17852
  };
17758
17853
  };
17759
17854
 
17855
+ // A scoped child's item exists only inside its owner: two owners can each hold
17856
+ // one with the same id. The callbacks of a relation chained on it whose result
17857
+ // names that item do so by its id alone — `.one()`/`.many()` as the parent
17858
+ // object or id, `.scopedOne()`/`.scopedMany()` as the `ownerId` of
17859
+ // `[ownerId, …]` — so applied they would write the owner's store (`store` of a
17860
+ // scoped child) or the scope of another owner's item. They are refused rather
17861
+ // than routed: routing needs the owner in the result, a contract none of them
17862
+ // has.
17863
+ const refuseCallbacksNamingScopedItem = (
17864
+ relationLabel,
17865
+ callbacks,
17866
+ { name, ownerName, propertyName, declarationSite },
17867
+ ) => {
17868
+ const declaredKeys = [];
17869
+ for (const key of Object.keys(callbacks)) {
17870
+ if (callbacks[key]) {
17871
+ declaredKeys.push(key);
17872
+ }
17873
+ }
17874
+ if (declaredKeys.length === 0) {
17875
+ return;
17876
+ }
17877
+ const declared = declaredKeys.join(", ");
17878
+ throw new Error(
17879
+ `${relationLabel} cannot declare ${declared}: a "${name}" item exists only inside its "${ownerName}", and the result of ${declared} names it without saying which "${ownerName}" holds it.
17880
+ Return "${propertyName}" embedded in what the "${name}" callbacks return ([ownerId, { …, ${propertyName} }]): those say which "${ownerName}".
17881
+ ${relationLabel} source location: ${declarationSite}`,
17882
+ );
17883
+ };
17884
+
17760
17885
  // Shared by .many() and .scopedMany(): converts a raw relationship value (an
17761
17886
  // array of child props/ids, or anything else meaning "empty") into an array of
17762
17887
  // child ids, upserting each entry into the child store. The id array signal is
@@ -17898,22 +18023,28 @@ Received an object with keys: ${keys.join(", ")}.`,
17898
18023
  * // which in turn triggers the route Signal->URL sync and updates the browser URL.
17899
18024
  */
17900
18025
  const syncResourceToSignals = (resource, propertyToSignalMap) => {
17901
- if (resource.getChildStore) {
18026
+ if (resource.ownerName) {
18027
+ // A scoped child's `store` is its owner's: watching it would sync the
18028
+ // owner items' properties, not the child's.
17902
18029
  throw new Error(
17903
- `syncResourceToSignals: "${resource.name}" is a scoped resource (scopedMany/scopedOne). Use syncOwnedResourceToSignals instead.`,
18030
+ `syncResourceToSignals: "${resource.name}" lives inside each "${resource.ownerName}", so it has no store of its own to watch.${resource.getChildStore ? " Use syncOwnedResourceToSignals instead." : ""}`,
17904
18031
  );
17905
18032
  }
17906
18033
  syncStoreToSignals(resource.store, propertyToSignalMap);
17907
18034
  };
17908
18035
 
17909
18036
  /**
17910
- * The same, for a `scopedOne`/`scopedMany` resource: its items live in one store
17911
- * per owner, so the store to watch is the one of the owner `ownerSignal` names
17912
- * (by id or by any unique key), and it is switched when the signal changes.
17913
- * Nothing is synced while `ownerSignal` holds `null`/`undefined` or names an
17914
- * owner nothing has been loaded for yet.
18037
+ * The same, for a `scopedMany` resource (or a `withParams()` of one): its items
18038
+ * live in one store per owner, so the store to watch is the one of the owner
18039
+ * `ownerSignal` names (by id or by any unique key), and it is switched when the
18040
+ * signal changes. Nothing is synced while `ownerSignal` holds `null`/`undefined`
18041
+ * or names an owner nothing has been loaded for yet.
17915
18042
  *
17916
- * @param {Object} resource - a resource made by `.scopedOne()` / `.scopedMany()`
18043
+ * A `scopedOne` child has no store (each owner holds its one item), and a
18044
+ * `scopedMany` declared on a scoped child is kept per item with no owner key to
18045
+ * find it by: both are refused.
18046
+ *
18047
+ * @param {Object} resource - a resource made by `.scopedMany()`
17917
18048
  * @param {import("@preact/signals").Signal} ownerSignal - the owner whose children are watched
17918
18049
  * @param {Object} propertyToSignalMap - `{ [propertyName]: signal }`, as for `syncResourceToSignals`
17919
18050
  */
@@ -17923,8 +18054,13 @@ const syncOwnedResourceToSignals = (
17923
18054
  propertyToSignalMap,
17924
18055
  ) => {
17925
18056
  if (!resource.getChildStore) {
18057
+ if (!resource.ownerName) {
18058
+ throw new Error(
18059
+ `syncOwnedResourceToSignals: "${resource.name}" is not a scoped resource. Use syncResourceToSignals instead.`,
18060
+ );
18061
+ }
17926
18062
  throw new Error(
17927
- `syncOwnedResourceToSignals: "${resource.name}" is not a scoped resource (scopedMany/scopedOne). Use syncResourceToSignals instead.`,
18063
+ `syncOwnedResourceToSignals: "${resource.name}" has no store per "${resource.ownerName}" to watch: a scopedOne child is held by each owner, and a scopedMany declared on a scoped child has no owner key to find its store by.`,
17928
18064
  );
17929
18065
  }
17930
18066
  effect(() => {
@@ -30219,7 +30355,7 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
30219
30355
  clip-path: var(--wall-holes-arriving, var(--wall-holes));
30220
30356
  }
30221
30357
 
30222
- :root[data-navi-route-transition] {
30358
+ :root[data-navi-route-transition-target="area"] {
30223
30359
  & .navi_dialog::backdrop, & .navi_popover_backdrop {
30224
30360
  backdrop-filter: none;
30225
30361
  background: none;
@@ -30364,10 +30500,7 @@ const nameFurnitureAround = (areaElement) => {
30364
30500
  if (!name) {
30365
30501
  // A name the application wrote itself answers for that element, and it
30366
30502
  // is saying something navi is not: it wants that bar moved on the pages'
30367
- // clock, by its own CSS. Asked only of a bar navi has never named — a
30368
- // bar wearing one of ITS names is one this movement inherited from the
30369
- // one it interrupted, and taking it for the application's would leave it
30370
- // named for the rest of the document's life.
30503
+ // clock, by its own CSS.
30371
30504
  if (getComputedStyle(element).viewTransitionName !== "none") {
30372
30505
  continue;
30373
30506
  }
@@ -32189,8 +32322,17 @@ const beginTransition = ({
32189
32322
  url: absoluteUrl(url),
32190
32323
  decision: navigationDecision,
32191
32324
  walkHome: null,
32192
- releaseReverting: null
32325
+ releaseReverting: null,
32326
+ restoreDuration: null
32193
32327
  };
32328
+ // The document wears one transition at a time, and the one this interrupts
32329
+ // ends only after this one has begun — too late to take off what it wears
32330
+ // (see end). Taking over therefore starts by taking it off, so that what is
32331
+ // put on below describes this transition alone, including what it leaves
32332
+ // unset: a type, an area, a duration.
32333
+ if (currentTransition) {
32334
+ releaseTransitionRoot(currentTransition);
32335
+ }
32194
32336
  currentTransition = transition;
32195
32337
  // Said before the picture is taken: whoever names something for a movement
32196
32338
  // between two pages decides on it now (see transition_destination.js).
@@ -32234,11 +32376,10 @@ const beginTransition = ({
32234
32376
  // A duration of this relation's own, worn for the length of the transition —
32235
32377
  // and whatever the application had written inline put back afterwards, not
32236
32378
  // erased.
32237
- let restoreDuration = null;
32238
32379
  if (duration !== undefined) {
32239
32380
  const durationBefore = documentElement.style.getPropertyValue(TRANSITION_DURATION_PROPERTY);
32240
32381
  documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, typeof duration === "number" ? `${duration}ms` : duration);
32241
- restoreDuration = () => {
32382
+ transition.restoreDuration = () => {
32242
32383
  if (durationBefore) {
32243
32384
  documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, durationBefore);
32244
32385
  } else {
@@ -32299,8 +32440,11 @@ const beginTransition = ({
32299
32440
  renderWait.stop();
32300
32441
  }
32301
32442
  // The page arriving is in the DOM and the transition has not started
32302
- // playing: the one moment both states of the area can be known.
32303
- if (areaElement) {
32443
+ // playing: the one moment both states of the area can be known. The
32444
+ // browser runs this callback even for a transition skipped before its
32445
+ // first picture — the next one starting skips it — and a transition
32446
+ // replaced holds nothing: what it wore was taken off at the takeover.
32447
+ if (areaElement && currentTransition === transition) {
32304
32448
  holdTransitionFurniture(transition, areaElement);
32305
32449
  holdTransitionWindow(transition, areaElement, areaStateBefore);
32306
32450
  }
@@ -32309,8 +32453,9 @@ const beginTransition = ({
32309
32453
  // Whatever ends it — played out, skipped by another transition starting,
32310
32454
  // failed before its callback ever ran — the hold is given back and the
32311
32455
  // document is handed back to the application. Both are idempotent, and
32312
- // the attributes belong to the LAST transition begun: an earlier one
32313
- // ending late must not strip what a later one is wearing.
32456
+ // the document is handed back only by the LAST transition begun: an
32457
+ // earlier one ending late was taken off by the one that replaced it, and
32458
+ // must not strip what that one is wearing.
32314
32459
  renderWait.stop();
32315
32460
  releaseRendering();
32316
32461
  // The hold a way back took, when it is still standing: the pictures were
@@ -32322,16 +32467,7 @@ const beginTransition = ({
32322
32467
  }
32323
32468
  if (currentTransition === transition) {
32324
32469
  currentTransition = null;
32325
- documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
32326
- documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
32327
- documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
32328
- releaseTransitionWindow(transition);
32329
- releaseTransitionDestination(transition);
32330
- releaseTransitionFurniture(transition);
32331
- releaseTransitionPress(transition);
32332
- if (restoreDuration) {
32333
- restoreDuration();
32334
- }
32470
+ releaseTransitionRoot(transition);
32335
32471
  }
32336
32472
  };
32337
32473
  transition.viewTransition = viewTransition;
@@ -32339,6 +32475,22 @@ const beginTransition = ({
32339
32475
  viewTransition.finished.then(end, end);
32340
32476
  };
32341
32477
 
32478
+ // Everything a transition wears on the document for its length, taken off by
32479
+ // its own end or by the transition taking over from it.
32480
+ const releaseTransitionRoot = transition => {
32481
+ const documentElement = document.documentElement;
32482
+ documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
32483
+ documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
32484
+ documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
32485
+ releaseTransitionWindow(transition);
32486
+ releaseTransitionDestination(transition);
32487
+ releaseTransitionFurniture(transition);
32488
+ releaseTransitionPress(transition);
32489
+ if (transition.restoreDuration) {
32490
+ transition.restoreDuration();
32491
+ }
32492
+ };
32493
+
32342
32494
  /**
32343
32495
  * The movement playing, turned round rather than replaced: this navigation is
32344
32496
  * exactly the way back of the one on screen — the door pressed again while the
@@ -74139,11 +74291,15 @@ const PickerNaviTime = props => {
74139
74291
  max = "23:30",
74140
74292
  step
74141
74293
  } = props;
74142
- const stepSeconds = timeStringToSeconds(step) ?? 1800;
74294
+ // resolveInputProps has already run (PickerFirstResolver) and left navi_time
74295
+ // as written: what is handed on is a time input, whose step is in seconds.
74296
+ const hostStep = timeStringToSeconds(step);
74297
+ const stepSeconds = hostStep ?? 1800;
74143
74298
  const slots = useMemo(() => generateTimeSlots(min, max, stepSeconds), [min, max, stepSeconds]);
74144
74299
  return jsx(Next, {
74145
74300
  ...props,
74146
74301
  type: "time",
74302
+ step: hostStep,
74147
74303
  children: jsx(List, {
74148
74304
  selectable: true,
74149
74305
  command: "--navi-send",
@@ -77189,6 +77345,10 @@ const warnOnUnknownPickerType = props => {
77189
77345
  pickerTypeWarnedSet.add(type);
77190
77346
  console.warn(`[navi] <Picker type="${type}"> — "${type}" is not a picker type. ` + `The picker holds a single text value instead, so an object given to it is written as "[object Object]". ` + `Types are: date, month, week, time, datetime, duration, color, file, text, array, object, confirm — ` + `"object" being the one for a popup holding several named controls.`);
77191
77347
  };
77348
+
77349
+ // PickerFirstResolver comes before the presets so a preset reads the
77350
+ // min/max/step a bound signal fills in (resolveInputProps); a preset type
77351
+ // therefore has no row in NAVI_TYPE_DEFAULTS (resolve_input_props.js).
77192
77352
  const PickerResolved = /*#__PURE__*/createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
77193
77353
  const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
77194
77354
  Date: PickerDateUI,