@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.
@@ -4794,14 +4794,11 @@ const CONTROL_TYPE_BY_SIGNAL_TYPE = {
4794
4794
  // become something plainer — it is what says the value is a number (see
4795
4795
  // isNumberInput), and what lets constraint messages use domain-specific
4796
4796
  // wording instead of the generic "Ce nombre doit être...".
4797
+ // A type a picker preset draws (navi_time, navi_minute: see picker_preset.jsx)
4798
+ // has no row: resolveInputProps runs before the preset resolver, and a row
4799
+ // would turn the type into its host before the preset resolver reads it. The
4800
+ // preset hands the host type on itself.
4797
4801
  const NAVI_TYPE_DEFAULTS = {
4798
- navi_time: {
4799
- "type": "time",
4800
- "navi-input-type": "time",
4801
- "min": 0,
4802
- "max": 24 * 3600 - 1,
4803
- "step": 1,
4804
- },
4805
4802
  navi_percentage: {
4806
4803
  "type": "navi_number",
4807
4804
  "navi-input-type": "percentage",
@@ -4863,7 +4860,6 @@ const isNumberInput = (type, naviInputType) => {
4863
4860
  * Supported navi types and their targets:
4864
4861
  * - `navi_percentage` → `navi_number` (0–100, step 1)
4865
4862
  * - `navi_number` → `text` (inputMode="numeric", no spin buttons implied)
4866
- * - `navi_time` → `time` (step in seconds)
4867
4863
  *
4868
4864
  * Standard HTML input types with formatters:
4869
4865
  * - `date`, `month`, `week`, `time`, `datetime-local`, `datetime`:
@@ -16014,6 +16010,15 @@ const createResource = (
16014
16010
  paramScope,
16015
16011
  rerunOn,
16016
16012
  dependencies,
16013
+ // Set on a scoped child: the resource owning each of its items, which
16014
+ // live in one store per owner (`store` is the owner's).
16015
+ ownerName,
16016
+ // Set on a scopedMany child: its per-owner store looked up by owner key,
16017
+ // and the owner whose GET embeds the collection. Options rather than
16018
+ // properties added afterwards, so every facade of the relation — the one
16019
+ // scopedMany returns and each withParams() of it — carries both.
16020
+ getChildStore,
16021
+ ownerDependency,
16017
16022
  },
16018
16023
  ) => {
16019
16024
  const params = paramScope.params;
@@ -16039,6 +16044,8 @@ const createResource = (
16039
16044
  // private but exposed for convenience
16040
16045
  store,
16041
16046
  addItemSetup,
16047
+ ownerName,
16048
+ getChildStore,
16042
16049
  };
16043
16050
 
16044
16051
  resourceLifecycleManager.registerResource(stateFacade, {
@@ -16047,6 +16054,17 @@ const createResource = (
16047
16054
  dependencies,
16048
16055
  uniqueKeys,
16049
16056
  });
16057
+ if (ownerDependency) {
16058
+ // A child collection mutated (POST etc.) makes the owner GET re-fetch:
16059
+ // the owner embeds the child array and only the backend knows the new
16060
+ // ordering. Registered per facade, since the rerun is looked up by the
16061
+ // facade whose action completed.
16062
+ resourceLifecycleManager.addDependency(
16063
+ stateFacade,
16064
+ ownerDependency.owner,
16065
+ ownerDependency.propertyName,
16066
+ );
16067
+ }
16050
16068
  const onActionComplete = (actionCompleted) => {
16051
16069
  resourceLifecycleManager.onActionComplete(actionCompleted, {
16052
16070
  resourceScope: stateFacade,
@@ -16059,6 +16077,10 @@ const createResource = (
16059
16077
  * Actions from parameterized resources only trigger rerun/reset for other actions with
16060
16078
  * identical parameters, preventing cross-contamination between different parameter sets.
16061
16079
  *
16080
+ * The actions are the resource's own kind: on a relation they keep its callback return
16081
+ * contract and write where it writes (`TABLE_COLUMNS.withParams(…).GET_MANY` returns
16082
+ * `[ownerId, items]` and replaces that owner's collection).
16083
+ *
16062
16084
  * @param {Object} params - Parameters to bind to all actions of this resource (required)
16063
16085
  * @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
16064
16086
  * @returns {Object} A new resource instance with parameter-bound actions and isolated lifecycle
@@ -16084,7 +16106,6 @@ const createResource = (
16084
16106
  paramsToInject,
16085
16107
  { dependencies: withParamsDeps, rerunOn: withParamsRerunOn } = {},
16086
16108
  ) => {
16087
- const declarationSite = getDeclarationSite();
16088
16109
  if (!paramsToInject || Object.keys(paramsToInject).length === 0) {
16089
16110
  throw new Error(`resource(${name}).withParams() requires parameters`);
16090
16111
  }
@@ -16092,11 +16113,6 @@ const createResource = (
16092
16113
  ? { ...params, ...paramsToInject }
16093
16114
  : paramsToInject;
16094
16115
  const resolvedParamScope = getParamScope(resolvedParams);
16095
- const createRestActionWithParams = createRestActionFactoryForRoot(name, {
16096
- idKey,
16097
- store,
16098
- declarationSite,
16099
- });
16100
16116
  return createResource(name, {
16101
16117
  idKey,
16102
16118
  uniqueKeys,
@@ -16104,10 +16120,16 @@ const createResource = (
16104
16120
  restCallbacks,
16105
16121
  store,
16106
16122
  addItemSetup,
16107
- createRestAction: createRestActionWithParams,
16123
+ // The actions of this resource's kind: a relation applies its results to
16124
+ // its relation (a scoped child to the per-owner stores), never as root
16125
+ // items of `store`.
16126
+ createRestAction,
16108
16127
  paramScope: resolvedParamScope,
16109
16128
  rerunOn: resolveRerunOn(withParamsRerunOn, rerunOn),
16110
16129
  dependencies: withParamsDeps ?? dependencies,
16130
+ ownerName,
16131
+ getChildStore,
16132
+ ownerDependency,
16111
16133
  });
16112
16134
  };
16113
16135
  stateFacade.withParams = withParams;
@@ -16152,6 +16174,13 @@ const createResource = (
16152
16174
  } = {},
16153
16175
  ) => {
16154
16176
  const declarationSite = getDeclarationSite();
16177
+ if (ownerName) {
16178
+ refuseCallbacksNamingScopedItem(
16179
+ `${name}.one("${propertyName}")`,
16180
+ { GET, PUT, DELETE },
16181
+ { name, ownerName, propertyName, declarationSite },
16182
+ );
16183
+ }
16155
16184
  const childName = `${name}.${propertyName}`;
16156
16185
  const childIdKey = childResource.idKey;
16157
16186
  const childStore = childResource.store;
@@ -16212,7 +16241,11 @@ const createResource = (
16212
16241
  );
16213
16242
  });
16214
16243
 
16215
- const createRestActionForOne = (verb, callback, { onActionComplete }) => {
16244
+ const createRestActionForOne = (
16245
+ verb,
16246
+ callback,
16247
+ { onActionComplete, paramScope },
16248
+ ) => {
16216
16249
  const applyResultToValue =
16217
16250
  verb === "DELETE"
16218
16251
  ? (itemIdOrItemProps) => {
@@ -16327,6 +16360,13 @@ const createResource = (
16327
16360
  } = {},
16328
16361
  ) => {
16329
16362
  const declarationSite = getDeclarationSite();
16363
+ if (ownerName) {
16364
+ refuseCallbacksNamingScopedItem(
16365
+ `${name}.many("${propertyName}")`,
16366
+ { GET_MANY, DELETE, DELETE_MANY },
16367
+ { name, ownerName, propertyName, declarationSite },
16368
+ );
16369
+ }
16330
16370
  const childStore = childResource.store;
16331
16371
  const childIdKey = childResource.idKey;
16332
16372
  const childName = `${name}.${propertyName}`;
@@ -16358,21 +16398,23 @@ const createResource = (
16358
16398
  const createRestActionForMany = (
16359
16399
  verb,
16360
16400
  callback,
16361
- { isMany, onActionComplete },
16401
+ { isMany, onActionComplete, paramScope },
16362
16402
  ) => {
16363
16403
  if (!isMany) {
16364
16404
  return createRestActionAffectingOneItem(verb, callback, {
16365
16405
  onActionComplete,
16406
+ paramScope,
16366
16407
  });
16367
16408
  }
16368
16409
  return createRestActionAffectingManyItems(verb, callback, {
16369
16410
  onActionComplete,
16411
+ paramScope,
16370
16412
  });
16371
16413
  };
16372
16414
  const createRestActionAffectingOneItem = (
16373
16415
  verb,
16374
16416
  callback,
16375
- { onActionComplete },
16417
+ { onActionComplete, paramScope },
16376
16418
  ) => {
16377
16419
  const applyResultToValue =
16378
16420
  verb === "DELETE"
@@ -16435,7 +16477,7 @@ const createResource = (
16435
16477
  const createRestActionAffectingManyItems = (
16436
16478
  verb,
16437
16479
  callback,
16438
- { onActionComplete },
16480
+ { onActionComplete, paramScope },
16439
16481
  ) => {
16440
16482
  const applyResultToValue =
16441
16483
  verb === "GET"
@@ -16563,7 +16605,12 @@ const createResource = (
16563
16605
  * Mutations apply directly to the owner's signal, so the parent GET is never rerun.
16564
16606
  *
16565
16607
  * Returns the child relationship resource, itself chainable:
16566
- * `USER_PROFILE.one("theme", THEME)` adds a reactive `.theme` property on each profile.
16608
+ * `USER_PROFILE.one("theme", THEME)` adds a reactive `.theme` property on each profile,
16609
+ * fed by what this resource's callbacks embed (`[id, { bio, theme: { id: 3 } }]`). A
16610
+ * profile exists only inside its user, so the chained relation declares none of the
16611
+ * callbacks whose result names a profile (`.one()` GET/PUT/DELETE, `.many()`
16612
+ * GET_MANY/DELETE/DELETE_MANY, any `.scopedOne()`/`.scopedMany()` callback): such a
16613
+ * result cannot say which user holds it, and the declaration throws.
16567
16614
  *
16568
16615
  * @param {string} propertyName - property holding the sub-object on each owner item
16569
16616
  * @param {Object} [restCallbacks] - `{ idKey, rerunOn, dependencies, GET, POST, PUT, PATCH, DELETE }`
@@ -16583,6 +16630,14 @@ const createResource = (
16583
16630
  DELETE,
16584
16631
  } = {},
16585
16632
  ) => {
16633
+ const declarationSite = getDeclarationSite();
16634
+ if (ownerName) {
16635
+ refuseCallbacksNamingScopedItem(
16636
+ `${name}.scopedOne("${propertyName}")`,
16637
+ { GET, POST, PUT, PATCH, DELETE },
16638
+ { name, ownerName, propertyName, declarationSite },
16639
+ );
16640
+ }
16586
16641
  const childName = `${name}.${propertyName}`;
16587
16642
 
16588
16643
  // Callbacks added by chained .one()/.many() on the child resource,
@@ -16612,7 +16667,12 @@ const createResource = (
16612
16667
  childSignal.value = childItem; // first activation: null → childItem
16613
16668
  }
16614
16669
  };
16615
- applyPropsMap.set(ownerId, applyProps);
16670
+ if (!ownerName) {
16671
+ // Declared on a scoped child, an owner id is only unique inside its
16672
+ // own owner and no callback addresses it (see
16673
+ // refuseCallbacksNamingScopedItem): nothing is looked up by it.
16674
+ applyPropsMap.set(ownerId, applyProps);
16675
+ }
16616
16676
 
16617
16677
  applyProps(ownerItem[propertyName]);
16618
16678
 
@@ -16624,7 +16684,7 @@ const createResource = (
16624
16684
  const createRestActionForScopedOne = (
16625
16685
  verb,
16626
16686
  callback,
16627
- { onActionComplete },
16687
+ { onActionComplete, paramScope },
16628
16688
  ) => {
16629
16689
  const childActionName = `${childName}.${verb}`;
16630
16690
  return createAction(callback, {
@@ -16672,6 +16732,7 @@ const createResource = (
16672
16732
  paramScope,
16673
16733
  rerunOn: resolveRerunOn(scopedOneRerunOn, rerunOn),
16674
16734
  dependencies: scopedOneDependencies ?? dependencies,
16735
+ ownerName: name,
16675
16736
  });
16676
16737
  };
16677
16738
 
@@ -16699,7 +16760,14 @@ const createResource = (
16699
16760
  * `propertyName`), and the child's own GET_MANY reruns per its `rerunOn`.
16700
16761
  *
16701
16762
  * Returns the child relationship resource, itself chainable:
16702
- * `TABLE_COLUMNS.one("dataType", DATA_TYPE)` adds a reactive `.dataType` property on each column.
16763
+ * `TABLE_COLUMNS.one("dataType", DATA_TYPE)` adds a reactive `.dataType` property on each column,
16764
+ * fed by what this resource's callbacks embed (`[id, [{ name, dataType: { id: 2 } }]]`). A
16765
+ * column exists only inside its table — two tables can each have an `email` — so the chained
16766
+ * relation declares none of the callbacks whose result names a column (`.one()`
16767
+ * GET/PUT/DELETE, `.many()` GET_MANY/DELETE/DELETE_MANY, any `.scopedOne()`/`.scopedMany()`
16768
+ * callback): such a result cannot say which table holds it, and the declaration throws.
16769
+ * A `.scopedMany()` chained on it keeps one collection per column: `users.email` and
16770
+ * `admins.email` never share one.
16703
16771
  *
16704
16772
  * @param {string} propertyName - property holding the collection on each owner item
16705
16773
  * @param {Object} [restCallbacks] - `{ idKey, rerunOn, dependencies, GET, GET_MANY, POST, POST_MANY, PUT, PUT_MANY, PATCH, PATCH_MANY, DELETE, DELETE_MANY }`
@@ -16724,6 +16792,25 @@ const createResource = (
16724
16792
  DELETE_MANY,
16725
16793
  } = {},
16726
16794
  ) => {
16795
+ const declarationSite = getDeclarationSite();
16796
+ if (ownerName) {
16797
+ refuseCallbacksNamingScopedItem(
16798
+ `${name}.scopedMany("${propertyName}")`,
16799
+ {
16800
+ GET,
16801
+ GET_MANY,
16802
+ POST,
16803
+ POST_MANY,
16804
+ PUT,
16805
+ PUT_MANY,
16806
+ PATCH,
16807
+ PATCH_MANY,
16808
+ DELETE,
16809
+ DELETE_MANY,
16810
+ },
16811
+ { name, ownerName, propertyName, declarationSite },
16812
+ );
16813
+ }
16727
16814
  const childName = `${name}.${propertyName}`;
16728
16815
 
16729
16816
  // Callbacks added by chained .one()/.many() on the child resource,
@@ -16745,37 +16832,42 @@ const createResource = (
16745
16832
  return childItem;
16746
16833
  },
16747
16834
  });
16748
- const scope = { childStore, idArraySignal: signal([]) };
16749
- scopeMap.set(ownerKey, scope);
16750
- return scope;
16835
+ return { childStore, idArraySignal: signal([]) };
16751
16836
  };
16752
16837
  addItemSetup((item) => {
16753
16838
  const ownerId = item[idKey];
16754
-
16755
- // Reuse an existing scope if one was already created under a uniqueKey
16756
- // value (e.g. rows were fetched by tablename before the full table was loaded).
16757
- let scope = scopeMap.get(ownerId);
16758
- if (!scope) {
16839
+ let scope;
16840
+ if (ownerName) {
16841
+ // The owner is itself a scoped child: its id is only unique inside its
16842
+ // own owner, so the scope is the item's alone, never looked up by id
16843
+ // (no callback addresses it, see refuseCallbacksNamingScopedItem).
16844
+ scope = createScope(ownerId);
16845
+ } else {
16846
+ // Reuse an existing scope if one was already created under a uniqueKey
16847
+ // value (e.g. rows were fetched by tablename before the full table was loaded).
16848
+ scope = scopeMap.get(ownerId);
16849
+ if (!scope) {
16850
+ for (const uniqueKey of uniqueKeys) {
16851
+ const uniqueKeyValue = item[uniqueKey];
16852
+ if (uniqueKeyValue !== undefined && scopeMap.has(uniqueKeyValue)) {
16853
+ scope = scopeMap.get(uniqueKeyValue);
16854
+ break;
16855
+ }
16856
+ }
16857
+ }
16858
+ if (!scope) {
16859
+ scope = createScope(ownerId);
16860
+ }
16861
+ // Register the scope under the id and every uniqueKey value so that
16862
+ // resolveOwnerId can address it whichever key a callback returns.
16863
+ scopeMap.set(ownerId, scope);
16759
16864
  for (const uniqueKey of uniqueKeys) {
16760
16865
  const uniqueKeyValue = item[uniqueKey];
16761
- if (uniqueKeyValue !== undefined && scopeMap.has(uniqueKeyValue)) {
16762
- scope = scopeMap.get(uniqueKeyValue);
16763
- break;
16866
+ if (uniqueKeyValue !== undefined) {
16867
+ scopeMap.set(uniqueKeyValue, scope);
16764
16868
  }
16765
16869
  }
16766
16870
  }
16767
- if (!scope) {
16768
- scope = createScope(ownerId);
16769
- }
16770
- // Register the scope under the id and every uniqueKey value so that
16771
- // resolveOwnerId can address it whichever key a callback returns.
16772
- scopeMap.set(ownerId, scope);
16773
- for (const uniqueKey of uniqueKeys) {
16774
- const uniqueKeyValue = item[uniqueKey];
16775
- if (uniqueKeyValue !== undefined) {
16776
- scopeMap.set(uniqueKeyValue, scope);
16777
- }
16778
- }
16779
16871
 
16780
16872
  const { childStore, idArraySignal } = scope;
16781
16873
  const updateChildItemIdArray = createChildIdArrayUpdater(
@@ -16813,7 +16905,7 @@ const createResource = (
16813
16905
  const createRestActionForScopedMany = (
16814
16906
  verb,
16815
16907
  callback,
16816
- { isMany, onActionComplete },
16908
+ { isMany, onActionComplete, paramScope },
16817
16909
  ) => {
16818
16910
  const childActionName = `${childName}.${verb}`;
16819
16911
  return createAction(callback, {
@@ -16835,7 +16927,11 @@ const createResource = (
16835
16927
  );
16836
16928
  // Owner not in store yet: create the scope so actions can run before
16837
16929
  // the parent item has been loaded (e.g. rows fetched before their table).
16838
- const scope = scopeMap.get(ownerId) || createScope(ownerId);
16930
+ let scope = scopeMap.get(ownerId);
16931
+ if (!scope) {
16932
+ scope = createScope(ownerId);
16933
+ scopeMap.set(ownerId, scope);
16934
+ }
16839
16935
  const { childStore, idArraySignal } = scope;
16840
16936
 
16841
16937
  if (verb === "DELETE") {
@@ -16907,18 +17003,17 @@ const createResource = (
16907
17003
  paramScope,
16908
17004
  rerunOn: resolveRerunOn(scopedManyRerunOn, rerunOn),
16909
17005
  dependencies: scopedManyDependencies ?? dependencies,
17006
+ ownerName: name,
17007
+ // Declared on a scoped child, the scopes are kept per item and never
17008
+ // registered by id (see addItemSetup above): there is no owner key to
17009
+ // find one by.
17010
+ getChildStore: ownerName
17011
+ ? undefined
17012
+ : (ownerKey) => scopeMap.get(ownerKey)?.childStore,
17013
+ // scopedOne needs no such dependency: its mutation result carries the
17014
+ // updated object directly.
17015
+ ownerDependency: { owner: stateFacade, propertyName },
16910
17016
  });
16911
- // When a scoped child collection is mutated (POST etc.), the parent GET must
16912
- // re-fetch: the parent embeds the child array and only the backend knows the
16913
- // new ordering. (scopedOne does not need this: the mutation result contains
16914
- // the updated object directly.)
16915
- resourceLifecycleManager.addDependency(
16916
- childResource,
16917
- stateFacade,
16918
- propertyName,
16919
- );
16920
- childResource.getChildStore = (ownerKey) =>
16921
- scopeMap.get(ownerKey)?.childStore;
16922
17017
  return childResource;
16923
17018
  };
16924
17019
 
@@ -17139,7 +17234,7 @@ const getLastGetItemId = (action) => {
17139
17234
  };
17140
17235
 
17141
17236
  // Captures the "file:line:column" of the user code that invoked the public
17142
- // function (resource(), .one(), .many(), withParams, …), so invalid-result
17237
+ // function (resource(), .one(), .many(), .scopedOne(), …), so invalid-result
17143
17238
  // errors can point at where the callbacks were declared, not at this file.
17144
17239
  // Must be called directly from the public function: the stack offset accounts
17145
17240
  // for exactly two frames (getCallerInfo → getDeclarationSite → public fn → user code).
@@ -17160,6 +17255,36 @@ ${originalActionName} source location: ${declarationSite}`,
17160
17255
  };
17161
17256
  };
17162
17257
 
17258
+ // A scoped child's item exists only inside its owner: two owners can each hold
17259
+ // one with the same id. The callbacks of a relation chained on it whose result
17260
+ // names that item do so by its id alone — `.one()`/`.many()` as the parent
17261
+ // object or id, `.scopedOne()`/`.scopedMany()` as the `ownerId` of
17262
+ // `[ownerId, …]` — so applied they would write the owner's store (`store` of a
17263
+ // scoped child) or the scope of another owner's item. They are refused rather
17264
+ // than routed: routing needs the owner in the result, a contract none of them
17265
+ // has.
17266
+ const refuseCallbacksNamingScopedItem = (
17267
+ relationLabel,
17268
+ callbacks,
17269
+ { name, ownerName, propertyName, declarationSite },
17270
+ ) => {
17271
+ const declaredKeys = [];
17272
+ for (const key of Object.keys(callbacks)) {
17273
+ if (callbacks[key]) {
17274
+ declaredKeys.push(key);
17275
+ }
17276
+ }
17277
+ if (declaredKeys.length === 0) {
17278
+ return;
17279
+ }
17280
+ const declared = declaredKeys.join(", ");
17281
+ throw new Error(
17282
+ `${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.
17283
+ Return "${propertyName}" embedded in what the "${name}" callbacks return ([ownerId, { …, ${propertyName} }]): those say which "${ownerName}".
17284
+ ${relationLabel} source location: ${declarationSite}`,
17285
+ );
17286
+ };
17287
+
17163
17288
  // Shared by .many() and .scopedMany(): converts a raw relationship value (an
17164
17289
  // array of child props/ids, or anything else meaning "empty") into an array of
17165
17290
  // child ids, upserting each entry into the child store. The id array signal is
@@ -17301,22 +17426,28 @@ Received an object with keys: ${keys.join(", ")}.`,
17301
17426
  * // which in turn triggers the route Signal->URL sync and updates the browser URL.
17302
17427
  */
17303
17428
  const syncResourceToSignals = (resource, propertyToSignalMap) => {
17304
- if (resource.getChildStore) {
17429
+ if (resource.ownerName) {
17430
+ // A scoped child's `store` is its owner's: watching it would sync the
17431
+ // owner items' properties, not the child's.
17305
17432
  throw new Error(
17306
- `syncResourceToSignals: "${resource.name}" is a scoped resource (scopedMany/scopedOne). Use syncOwnedResourceToSignals instead.`,
17433
+ `syncResourceToSignals: "${resource.name}" lives inside each "${resource.ownerName}", so it has no store of its own to watch.${resource.getChildStore ? " Use syncOwnedResourceToSignals instead." : ""}`,
17307
17434
  );
17308
17435
  }
17309
17436
  syncStoreToSignals(resource.store, propertyToSignalMap);
17310
17437
  };
17311
17438
 
17312
17439
  /**
17313
- * The same, for a `scopedOne`/`scopedMany` resource: its items live in one store
17314
- * per owner, so the store to watch is the one of the owner `ownerSignal` names
17315
- * (by id or by any unique key), and it is switched when the signal changes.
17316
- * Nothing is synced while `ownerSignal` holds `null`/`undefined` or names an
17317
- * owner nothing has been loaded for yet.
17440
+ * The same, for a `scopedMany` resource (or a `withParams()` of one): its items
17441
+ * live in one store per owner, so the store to watch is the one of the owner
17442
+ * `ownerSignal` names (by id or by any unique key), and it is switched when the
17443
+ * signal changes. Nothing is synced while `ownerSignal` holds `null`/`undefined`
17444
+ * or names an owner nothing has been loaded for yet.
17318
17445
  *
17319
- * @param {Object} resource - a resource made by `.scopedOne()` / `.scopedMany()`
17446
+ * A `scopedOne` child has no store (each owner holds its one item), and a
17447
+ * `scopedMany` declared on a scoped child is kept per item with no owner key to
17448
+ * find it by: both are refused.
17449
+ *
17450
+ * @param {Object} resource - a resource made by `.scopedMany()`
17320
17451
  * @param {import("@preact/signals").Signal} ownerSignal - the owner whose children are watched
17321
17452
  * @param {Object} propertyToSignalMap - `{ [propertyName]: signal }`, as for `syncResourceToSignals`
17322
17453
  */
@@ -17326,8 +17457,13 @@ const syncOwnedResourceToSignals = (
17326
17457
  propertyToSignalMap,
17327
17458
  ) => {
17328
17459
  if (!resource.getChildStore) {
17460
+ if (!resource.ownerName) {
17461
+ throw new Error(
17462
+ `syncOwnedResourceToSignals: "${resource.name}" is not a scoped resource. Use syncResourceToSignals instead.`,
17463
+ );
17464
+ }
17329
17465
  throw new Error(
17330
- `syncOwnedResourceToSignals: "${resource.name}" is not a scoped resource (scopedMany/scopedOne). Use syncResourceToSignals instead.`,
17466
+ `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.`,
17331
17467
  );
17332
17468
  }
17333
17469
  effect(() => {
@@ -29313,7 +29449,7 @@ const TRANSITION_FURNITURE_CSS = /* css */ `.navi_fixed_bar[data-navi-transition
29313
29449
  clip-path: var(--wall-holes-arriving, var(--wall-holes));
29314
29450
  }
29315
29451
 
29316
- :root[data-navi-route-transition] {
29452
+ :root[data-navi-route-transition-target="area"] {
29317
29453
  & .navi_dialog::backdrop, & .navi_popover_backdrop {
29318
29454
  backdrop-filter: none;
29319
29455
  background: none;
@@ -29458,10 +29594,7 @@ const nameFurnitureAround = (areaElement) => {
29458
29594
  if (!name) {
29459
29595
  // A name the application wrote itself answers for that element, and it
29460
29596
  // is saying something navi is not: it wants that bar moved on the pages'
29461
- // clock, by its own CSS. Asked only of a bar navi has never named — a
29462
- // bar wearing one of ITS names is one this movement inherited from the
29463
- // one it interrupted, and taking it for the application's would leave it
29464
- // named for the rest of the document's life.
29597
+ // clock, by its own CSS.
29465
29598
  if (getComputedStyle(element).viewTransitionName !== "none") {
29466
29599
  continue;
29467
29600
  }
@@ -31283,8 +31416,17 @@ const beginTransition = ({
31283
31416
  url: absoluteUrl(url),
31284
31417
  decision: navigationDecision,
31285
31418
  walkHome: null,
31286
- releaseReverting: null
31419
+ releaseReverting: null,
31420
+ restoreDuration: null
31287
31421
  };
31422
+ // The document wears one transition at a time, and the one this interrupts
31423
+ // ends only after this one has begun — too late to take off what it wears
31424
+ // (see end). Taking over therefore starts by taking it off, so that what is
31425
+ // put on below describes this transition alone, including what it leaves
31426
+ // unset: a type, an area, a duration.
31427
+ if (currentTransition) {
31428
+ releaseTransitionRoot(currentTransition);
31429
+ }
31288
31430
  currentTransition = transition;
31289
31431
  // Said before the picture is taken: whoever names something for a movement
31290
31432
  // between two pages decides on it now (see transition_destination.js).
@@ -31328,11 +31470,10 @@ const beginTransition = ({
31328
31470
  // A duration of this relation's own, worn for the length of the transition —
31329
31471
  // and whatever the application had written inline put back afterwards, not
31330
31472
  // erased.
31331
- let restoreDuration = null;
31332
31473
  if (duration !== undefined) {
31333
31474
  const durationBefore = documentElement.style.getPropertyValue(TRANSITION_DURATION_PROPERTY);
31334
31475
  documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, typeof duration === "number" ? `${duration}ms` : duration);
31335
- restoreDuration = () => {
31476
+ transition.restoreDuration = () => {
31336
31477
  if (durationBefore) {
31337
31478
  documentElement.style.setProperty(TRANSITION_DURATION_PROPERTY, durationBefore);
31338
31479
  } else {
@@ -31393,8 +31534,11 @@ const beginTransition = ({
31393
31534
  renderWait.stop();
31394
31535
  }
31395
31536
  // The page arriving is in the DOM and the transition has not started
31396
- // playing: the one moment both states of the area can be known.
31397
- if (areaElement) {
31537
+ // playing: the one moment both states of the area can be known. The
31538
+ // browser runs this callback even for a transition skipped before its
31539
+ // first picture — the next one starting skips it — and a transition
31540
+ // replaced holds nothing: what it wore was taken off at the takeover.
31541
+ if (areaElement && currentTransition === transition) {
31398
31542
  holdTransitionFurniture(transition, areaElement);
31399
31543
  holdTransitionWindow(transition, areaElement, areaStateBefore);
31400
31544
  }
@@ -31403,8 +31547,9 @@ const beginTransition = ({
31403
31547
  // Whatever ends it — played out, skipped by another transition starting,
31404
31548
  // failed before its callback ever ran — the hold is given back and the
31405
31549
  // document is handed back to the application. Both are idempotent, and
31406
- // the attributes belong to the LAST transition begun: an earlier one
31407
- // ending late must not strip what a later one is wearing.
31550
+ // the document is handed back only by the LAST transition begun: an
31551
+ // earlier one ending late was taken off by the one that replaced it, and
31552
+ // must not strip what that one is wearing.
31408
31553
  renderWait.stop();
31409
31554
  releaseRendering();
31410
31555
  // The hold a way back took, when it is still standing: the pictures were
@@ -31416,16 +31561,7 @@ const beginTransition = ({
31416
31561
  }
31417
31562
  if (currentTransition === transition) {
31418
31563
  currentTransition = null;
31419
- documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
31420
- documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
31421
- documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
31422
- releaseTransitionWindow(transition);
31423
- releaseTransitionDestination(transition);
31424
- releaseTransitionFurniture(transition);
31425
- releaseTransitionPress(transition);
31426
- if (restoreDuration) {
31427
- restoreDuration();
31428
- }
31564
+ releaseTransitionRoot(transition);
31429
31565
  }
31430
31566
  };
31431
31567
  transition.viewTransition = viewTransition;
@@ -31433,6 +31569,22 @@ const beginTransition = ({
31433
31569
  viewTransition.finished.then(end, end);
31434
31570
  };
31435
31571
 
31572
+ // Everything a transition wears on the document for its length, taken off by
31573
+ // its own end or by the transition taking over from it.
31574
+ const releaseTransitionRoot = transition => {
31575
+ const documentElement = document.documentElement;
31576
+ documentElement.removeAttribute(TRANSITION_ATTRIBUTE);
31577
+ documentElement.removeAttribute(TRANSITION_TYPE_ATTRIBUTE);
31578
+ documentElement.removeAttribute(TRANSITION_TARGET_ATTRIBUTE);
31579
+ releaseTransitionWindow(transition);
31580
+ releaseTransitionDestination(transition);
31581
+ releaseTransitionFurniture(transition);
31582
+ releaseTransitionPress(transition);
31583
+ if (transition.restoreDuration) {
31584
+ transition.restoreDuration();
31585
+ }
31586
+ };
31587
+
31436
31588
  /**
31437
31589
  * The movement playing, turned round rather than replaced: this navigation is
31438
31590
  * exactly the way back of the one on screen — the door pressed again while the
@@ -72675,11 +72827,15 @@ const PickerNaviTime = props => {
72675
72827
  max = "23:30",
72676
72828
  step
72677
72829
  } = props;
72678
- const stepSeconds = timeStringToSeconds(step) ?? 1800;
72830
+ // resolveInputProps has already run (PickerFirstResolver) and left navi_time
72831
+ // as written: what is handed on is a time input, whose step is in seconds.
72832
+ const hostStep = timeStringToSeconds(step);
72833
+ const stepSeconds = hostStep ?? 1800;
72679
72834
  const slots = useMemo(() => generateTimeSlots(min, max, stepSeconds), [min, max, stepSeconds]);
72680
72835
  return jsx(Next, {
72681
72836
  ...props,
72682
72837
  type: "time",
72838
+ step: hostStep,
72683
72839
  children: jsx(List, {
72684
72840
  selectable: true,
72685
72841
  command: "--navi-send",
@@ -75091,6 +75247,10 @@ const PickerFirstResolver = props => {
75091
75247
  ...props
75092
75248
  });
75093
75249
  };
75250
+
75251
+ // PickerFirstResolver comes before the presets so a preset reads the
75252
+ // min/max/step a bound signal fills in (resolveInputProps); a preset type
75253
+ // therefore has no row in NAVI_TYPE_DEFAULTS (resolve_input_props.js).
75094
75254
  const PickerResolved = /*#__PURE__*/createComponentResolver([PickerFirstResolver, PickerPresetResolver, PickerConfirmResolver, PickerCustomResolver, PickerTypeResolver, PickerButton]);
75095
75255
  const PickerUI = /*#__PURE__*/Object.assign(PickerDefaultUI, {
75096
75256
  Date: PickerDateUI,