@jsenv/navi 0.29.97 → 0.29.100

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.
@@ -38,7 +38,7 @@ installImportMetaCssBuild(import.meta);/**
38
38
  * any of these, and a number is the last resort, not the first tool.
39
39
  */
40
40
 
41
- const css$14 = /* css */`
41
+ const css$15 = /* css */`
42
42
  @layer navi {
43
43
  :root {
44
44
  /* A control that overlaps its neighbours (the members of a Group share
@@ -92,7 +92,7 @@ const css$14 = /* css */`
92
92
  }
93
93
  }
94
94
  `;
95
- import.meta.css = [css$14, "@jsenv/navi/src/navi_z_indexes.js"];
95
+ import.meta.css = [css$15, "@jsenv/navi/src/navi_z_indexes.js"];
96
96
 
97
97
  const addIntoArray = (array, ...valuesToAdd) => {
98
98
  if (valuesToAdd.length === 1) {
@@ -361,7 +361,7 @@ installImportMetaCssBuild(import.meta);/**
361
361
  * the very first render and the browser does everything on its own.
362
362
  */
363
363
  const URL_TARGET_ATTRIBUTE = "data-url-target";
364
- const css$13 = /* css */`
364
+ const css$14 = /* css */`
365
365
  @layer navi {
366
366
  [${URL_TARGET_ATTRIBUTE}] {
367
367
  animation: navi_url_target var(--navi-url-target-duration, 2000ms)
@@ -379,7 +379,7 @@ const css$13 = /* css */`
379
379
  }
380
380
  }
381
381
  `;
382
- import.meta.css = [css$13, "@jsenv/navi/src/nav/url_target/url_target.js"];
382
+ import.meta.css = [css$14, "@jsenv/navi/src/nav/url_target/url_target.js"];
383
383
  let urlTargetOptions = {
384
384
  block: "start",
385
385
  behavior: "instant",
@@ -6403,7 +6403,7 @@ installImportMetaCssBuild(import.meta);/**
6403
6403
  * - Arrow automatically shows when pointing at a valid anchor element
6404
6404
  * - Centers in viewport when no anchor element provided or anchor is too big
6405
6405
  */
6406
- const css$12 = /* css */`
6406
+ const css$13 = /* css */`
6407
6407
  @layer navi {
6408
6408
  .navi_callout {
6409
6409
  /* A callout is parented to what it explains, so it inherits from it — and
@@ -6646,7 +6646,7 @@ const openCallout = (message, {
6646
6646
  skipFocus = false,
6647
6647
  debug = () => {}
6648
6648
  } = {}) => {
6649
- import.meta.css = [css$12, "@jsenv/navi/src/control/rules/callout/callout.js"];
6649
+ import.meta.css = [css$13, "@jsenv/navi/src/control/rules/callout/callout.js"];
6650
6650
  if (debug === true) {
6651
6651
  debug = (e, ...args) => console.debug(`"${e.type}" -> `, ...args);
6652
6652
  }
@@ -8012,12 +8012,20 @@ const isControlBusy = (field) => {
8012
8012
  // An optimistic control stays interactive while its bound action runs:
8013
8013
  // a new interaction is queued behind the run (see the action queue in
8014
8014
  // control_hooks.jsx) rather than refused.
8015
- if (
8016
- !field.optimistic &&
8017
- boundAction &&
8018
- boundAction.runningStateSignal.value === RUNNING
8019
- ) {
8020
- return true;
8015
+ if (!field.optimistic && boundAction) {
8016
+ // The INSTANCE the proxy resolves to right now, not the proxy's own
8017
+ // signal: that one is a MIRROR, synced by an effect the settling batch
8018
+ // defers — read mid-batch (a state echo carrying the user's event back
8019
+ // down, an automatic follow-up), it still says RUNNING for an action
8020
+ // that is already over, and the gate would refuse — callout included —
8021
+ // for nothing. The resolved instance is the live truth: at that echo it
8022
+ // is the instance that just settled, already COMPLETED. And it IS the
8023
+ // running one whenever one runs — a non-optimistic control's state
8024
+ // cannot move mid-run, this very gate blocks it.
8025
+ const liveAction = boundAction.getCurrentAction?.() ?? boundAction;
8026
+ if (liveAction.runningStateSignal.value === RUNNING) {
8027
+ return true;
8028
+ }
8021
8029
  }
8022
8030
  if (field.loadingFromParent) {
8023
8031
  const parent = field.parentUIStateController;
@@ -8940,10 +8948,12 @@ const REQUIRED_CONSTRAINT = {
8940
8948
  };
8941
8949
  }
8942
8950
 
8943
- // checkbox_group controller: check aggregate uiState array
8951
+ // checkbox_group controller: check aggregate uiState array. An empty
8952
+ // selection aggregates to undefined, not to an empty array (see
8953
+ // GROUP_DEFAULTS.checkbox_group) — both mean "nothing checked" here.
8944
8954
  if (controlType === "checkbox_group") {
8945
8955
  const uiState = field.uiState;
8946
- if (uiState.length > 0) {
8956
+ if (uiState !== undefined && uiState.length > 0) {
8947
8957
  return null;
8948
8958
  }
8949
8959
  return {
@@ -11928,6 +11938,39 @@ const reportErrorIfNobodyDisplaysIt = (error, { action } = {}) => {
11928
11938
 
11929
11939
  const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
11930
11940
 
11941
+ /*
11942
+ * Actions: async callbacks wrapped in reactive state.
11943
+ *
11944
+ * An action owns a set of signals (params, runningState, error, value, data)
11945
+ * and moves through IDLE → RUNNING → COMPLETED / FAILED / ABORTED (see
11946
+ * action_run_states.js). `createAction(callback)` returns the root action;
11947
+ * `.bindParams(params)` derives child actions (one per params value, cached),
11948
+ * and binding a signal (or an object containing signals) returns an action
11949
+ * *proxy* that retargets itself to the right child action as the signal
11950
+ * changes (see createActionProxyFromSignal).
11951
+ *
11952
+ * How things run: prerun/run/rerun/reset never execute the action directly —
11953
+ * they go through `dispatchActions`, which the navigation integration can
11954
+ * replace via `setActionDispatcher` so that every action update participates
11955
+ * in the browser navigation lifecycle (abort signals, navigation events).
11956
+ * The default dispatcher calls `updateActions`, the single entry point that
11957
+ * resolves priorities between the four operation sets (reset > rerun > run >
11958
+ * prerun) and performs them.
11959
+ *
11960
+ * Memory design (the surprising part): nothing here keeps actions alive.
11961
+ * Child actions are held through ephemerons (createJsValueWeakMap) so a child
11962
+ * and its params are garbage-collected together, running actions live in
11963
+ * iterable *weak* sets, and property/signal mirroring uses weakEffect. Two
11964
+ * consequences to be aware of:
11965
+ * - an action can exist in several places only if everyone shares the same
11966
+ * instance (the caches above are what makes lookups return it);
11967
+ * - prerun actions may have no other reference yet, so
11968
+ * prerunProtectionRegistry pins them for a few minutes.
11969
+ *
11970
+ * An action run never throws: failures land in errorSignal and are reported
11971
+ * once, by one rule, in action_error_report.js (see the comment in onRunError).
11972
+ */
11973
+
11931
11974
  let DEBUG$1 = false;
11932
11975
  const enableDebugActions = () => {
11933
11976
  DEBUG$1 = true;
@@ -11966,7 +12009,7 @@ const getActionDispatcher = () => dispatchActions;
11966
12009
  const rerunActions = async (actionSet, options) => {
11967
12010
  return dispatchActions({
11968
12011
  rerunSet: actionSet,
11969
- reason: "rerunActions was calle",
12012
+ reason: "rerunActions was called",
11970
12013
  ...options,
11971
12014
  });
11972
12015
  };
@@ -11985,7 +12028,7 @@ const rerunActions = async (actionSet, options) => {
11985
12028
  */
11986
12029
  const prerunProtectionRegistry = (() => {
11987
12030
  const protectedActionMap = new Map(); // action -> { timeoutId, timestamp }
11988
- const PROTECTION_DURATION = 5 * 60 * 1000; // 5 minutes en millisecondes
12031
+ const PROTECTION_DURATION = 5 * 60 * 1000; // 5 minutes
11989
12032
 
11990
12033
  const unprotect = (action) => {
11991
12034
  const protection = protectedActionMap.get(action);
@@ -11999,7 +12042,7 @@ const prerunProtectionRegistry = (() => {
11999
12042
 
12000
12043
  return {
12001
12044
  protect(action) {
12002
- // Si déjà protégée, étendre la protection
12045
+ // already protected: extend the protection
12003
12046
  if (protectedActionMap.has(action)) {
12004
12047
  const existing = protectedActionMap.get(action);
12005
12048
  clearTimeout(existing.timeoutId);
@@ -12019,29 +12062,11 @@ const prerunProtectionRegistry = (() => {
12019
12062
  },
12020
12063
 
12021
12064
  unprotect,
12022
-
12023
- isProtected(action) {
12024
- return protectedActionMap.has(action);
12025
- },
12026
-
12027
- // Pour debugging
12028
- getProtectedActions() {
12029
- return Array.from(protectedActionMap.keys());
12030
- },
12031
-
12032
- // Nettoyage manuel si nécessaire
12033
- clear() {
12034
- for (const [, protection] of protectedActionMap) {
12035
- clearTimeout(protection.timeoutId);
12036
- }
12037
- protectedActionMap.clear();
12038
- },
12039
12065
  };
12040
12066
  })();
12041
12067
 
12042
12068
  const formatActionSet = (actionSet, prefix = "") => {
12043
- let message = "";
12044
- message += `${prefix}`;
12069
+ let message = prefix;
12045
12070
  for (const action of actionSet) {
12046
12071
  message += "\n";
12047
12072
  message += prefixFirstAndIndentRemainingLines(String(action), {
@@ -12376,7 +12401,6 @@ ${lines.join("\n")}`);
12376
12401
  };
12377
12402
 
12378
12403
  const NO_PARAMS = { __no_params__: true };
12379
- const initialParamsDefault = NO_PARAMS;
12380
12404
  const mergeActionParams = (currentParams, newParams) => {
12381
12405
  if (currentParams === NO_PARAMS) {
12382
12406
  return newParams;
@@ -12424,7 +12448,7 @@ const createAction = (callback, rootOptions = {}) => {
12424
12448
  } = options;
12425
12449
  if (!Object.hasOwn(options, "params")) {
12426
12450
  // even undefined should be respected it's only when not provided at all we use default
12427
- params = initialParamsDefault;
12451
+ params = NO_PARAMS;
12428
12452
  }
12429
12453
  if (value === undefined && data !== undefined) {
12430
12454
  value = data;
@@ -12437,11 +12461,7 @@ const createAction = (callback, rootOptions = {}) => {
12437
12461
  const errorSignal = signal(error);
12438
12462
  const valueSignal = signal(valueInitial);
12439
12463
  const dataSignal = valueToData
12440
- ? computed(() => {
12441
- const value = valueSignal.value;
12442
- const data = valueToData(value);
12443
- return data;
12444
- })
12464
+ ? computed(() => valueToData(valueSignal.value))
12445
12465
  : valueSignal;
12446
12466
 
12447
12467
  const prerun = (options) => {
@@ -12474,7 +12494,7 @@ const createAction = (callback, rootOptions = {}) => {
12474
12494
  return dispatchSingleAction(action, "reset", options);
12475
12495
  };
12476
12496
  const abort = (reason) => {
12477
- if (runningState !== RUNNING) {
12497
+ if (runningStateSignal.peek() !== RUNNING) {
12478
12498
  return false;
12479
12499
  }
12480
12500
  const actionAbort = actionAbortMap.get(action);
@@ -12500,7 +12520,7 @@ const createAction = (callback, rootOptions = {}) => {
12500
12520
  */
12501
12521
  const childActionWeakMap = createJsValueWeakMap();
12502
12522
  const _bindParams = (newParamsOrSignal, options = {}) => {
12503
- // CAS 1: Signal direct -> proxy
12523
+ // Case 1: a signal proxy that retargets as the signal changes
12504
12524
  if (isSignal(newParamsOrSignal)) {
12505
12525
  const combinedParamsSignal = computed(() => {
12506
12526
  const newParams = newParamsOrSignal.value;
@@ -12514,7 +12534,7 @@ const createAction = (callback, rootOptions = {}) => {
12514
12534
  );
12515
12535
  }
12516
12536
 
12517
- // CAS 2: Objet -> vérifier s'il contient des signals
12537
+ // Case 2: a plain object child action, or proxy when it contains signals
12518
12538
  if (isPlainObject$1(newParamsOrSignal)) {
12519
12539
  const staticParams = {};
12520
12540
  const signalMap = new Map();
@@ -12535,7 +12555,7 @@ const createAction = (callback, rootOptions = {}) => {
12535
12555
  }
12536
12556
 
12537
12557
  if (signalMap.size === 0) {
12538
- // Pas de signals, merge statique normal
12558
+ // no signals: plain static merge
12539
12559
  if (
12540
12560
  params === null ||
12541
12561
  typeof params !== "object" ||
@@ -12553,24 +12573,27 @@ const createAction = (callback, rootOptions = {}) => {
12553
12573
  });
12554
12574
  }
12555
12575
 
12556
- // Combiner avec les params existants pour les valeurs statiques
12557
- const paramsSignal = computed(() => {
12558
- const params = {};
12576
+ const combinedParamsSignal = computed(() => {
12577
+ const combinedParams = {};
12559
12578
  for (const key of keyArray) {
12560
12579
  const signalForThisKey = signalMap.get(key);
12561
12580
  if (signalForThisKey) {
12562
12581
  // eslint-disable-next-line signals/no-conditional-value-read
12563
- params[key] = signalForThisKey.value;
12582
+ combinedParams[key] = signalForThisKey.value;
12564
12583
  } else {
12565
- params[key] = staticParams[key];
12584
+ combinedParams[key] = staticParams[key];
12566
12585
  }
12567
12586
  }
12568
- return params;
12587
+ return combinedParams;
12569
12588
  });
12570
- return createActionProxyFromSignal(action, paramsSignal, options);
12589
+ return createActionProxyFromSignal(
12590
+ action,
12591
+ combinedParamsSignal,
12592
+ options,
12593
+ );
12571
12594
  }
12572
12595
 
12573
- // CAS 3: Primitive or objects like DOMEvents etc -> action enfant
12596
+ // Case 3: a primitive or non-plain object (DOM event, …) → child action
12574
12597
  return createChildAction({
12575
12598
  params: newParamsOrSignal,
12576
12599
  ...options,
@@ -12603,7 +12626,6 @@ const createAction = (callback, rootOptions = {}) => {
12603
12626
  return childAction;
12604
12627
  };
12605
12628
 
12606
- // ✅ Implement matchAllSelfOrDescendant
12607
12629
  const matchAllSelfOrDescendant = (predicate, { includeProxies } = {}) => {
12608
12630
  const matches = [];
12609
12631
 
@@ -12638,32 +12660,31 @@ const createAction = (callback, rootOptions = {}) => {
12638
12660
  generateActionCallSource(name, params),
12639
12661
  );
12640
12662
 
12641
- {
12642
- // Create the action as a function that can be called directly
12643
- action = function actionFunction(...args) {
12644
- if (args.length === 0) {
12645
- return action.rerun();
12646
- }
12647
- const boundAction = bindParams(...args);
12648
- return boundAction.rerun();
12649
- };
12650
- Object.defineProperty(action, "name", {
12651
- configurable: true,
12652
- get() {
12653
- return actionNameSignal.value;
12654
- },
12655
- });
12656
- Object.defineProperty(action, "callSource", {
12657
- configurable: true,
12658
- get() {
12659
- return actionCallSourceSignal.value;
12660
- },
12661
- set(v) {
12662
- actionCallSourceSignal.value = v;
12663
- },
12664
- });
12665
- actionWeakMap.set(action, action);
12666
- }
12663
+ // The action is a callable: `ACTION(params)` is `ACTION.bindParams(params).rerun()`
12664
+ action = function actionFunction(...args) {
12665
+ if (args.length === 0) {
12666
+ return action.rerun();
12667
+ }
12668
+ const boundAction = bindParams(...args);
12669
+ return boundAction.rerun();
12670
+ };
12671
+ Object.defineProperty(action, "name", {
12672
+ configurable: true,
12673
+ get() {
12674
+ return actionNameSignal.value;
12675
+ },
12676
+ });
12677
+ Object.defineProperty(action, "callSource", {
12678
+ configurable: true,
12679
+ get() {
12680
+ return actionCallSourceSignal.value;
12681
+ },
12682
+ set(v) {
12683
+ actionCallSourceSignal.value = v;
12684
+ },
12685
+ });
12686
+ // makes createAction(anAction) return the action itself
12687
+ actionWeakMap.set(action, action);
12667
12688
 
12668
12689
  // Assign all the action properties and methods to the function
12669
12690
  Object.assign(action, {
@@ -12685,7 +12706,7 @@ const createAction = (callback, rootOptions = {}) => {
12685
12706
  reset,
12686
12707
  abort,
12687
12708
  bindParams,
12688
- matchAllSelfOrDescendant, // ✅ Add the new method
12709
+ matchAllSelfOrDescendant,
12689
12710
  replaceParams: (newParams) => {
12690
12711
  const currentParams = paramsSignal.value;
12691
12712
  const nextParams = mergeActionParams(currentParams, newParams);
@@ -12713,7 +12734,7 @@ const createAction = (callback, rootOptions = {}) => {
12713
12734
  toString: () => action.callSource,
12714
12735
  meta,
12715
12736
  debug: (...args) => {
12716
- if (!meta.debug || DEBUG$1) {
12737
+ if (!meta.debug && !DEBUG$1) {
12717
12738
  return;
12718
12739
  }
12719
12740
  console.debug(...args);
@@ -12728,7 +12749,8 @@ const createAction = (callback, rootOptions = {}) => {
12728
12749
  });
12729
12750
  Object.preventExtensions(action);
12730
12751
 
12731
- // Effects pour synchroniser les propriétés
12752
+ // Mirror signals into plain properties (action.error, action.data, …)
12753
+ // so non-reactive code can read them without subscribing.
12732
12754
  {
12733
12755
  weakEffect([action], (actionRef) => {
12734
12756
  isPrerun = isPrerunSignal.value;
@@ -12754,7 +12776,6 @@ const createAction = (callback, rootOptions = {}) => {
12754
12776
  });
12755
12777
  }
12756
12778
 
12757
- // Propriétés privées
12758
12779
  {
12759
12780
  const ui = {
12760
12781
  renderLoaded: null,
@@ -13033,8 +13054,8 @@ const createAction = (callback, rootOptions = {}) => {
13033
13054
  * @param {boolean} options.rerunOnChange - Ensures the action is rerun every time a signal value is modified.
13034
13055
  * This enables live updates - for example, performing an HTTP GET request every time
13035
13056
  * a list of filters changes, providing real-time results without user interaction.
13036
- * @param {boolean} options.inheritData - When true, each new target action starts fresh with no inherited state.
13037
- * By default (false), the proxy carries over the previous target's value and error into the new action.
13057
+ * @param {boolean} options.inheritData - When false, each new target action starts fresh with no inherited state.
13058
+ * By default (true), the proxy carries over the previous target's value and error into the new action.
13038
13059
  * This keeps the facade in sync with the latest known data: `action.dataSignal.value` only changes when a
13039
13060
  * new action completes, not when it starts loading. Code that needs to distinguish loading state can still
13040
13061
  * check `action.runningState`, while code that just reads `action.data` always sees the most recent
@@ -13128,6 +13149,7 @@ const createActionProxyFromSignal = (
13128
13149
  currentAction = actionTarget;
13129
13150
  currentActionPrivateProperties = getActionPrivateProperties(actionTarget);
13130
13151
  }
13152
+
13131
13153
  actionTargetPreviousWeakRef = actionTarget
13132
13154
  ? new WeakRef(actionTarget)
13133
13155
  : null;
@@ -13153,25 +13175,22 @@ const createActionProxyFromSignal = (
13153
13175
 
13154
13176
  const nameSignal = signal(action.name);
13155
13177
  const callSourceSignal = signal(`[Proxy] ${action.callSource}`);
13156
- let actionProxy;
13157
- {
13158
- actionProxy = function actionProxyFunction() {
13159
- return actionProxy.rerun();
13160
- };
13161
- Object.defineProperty(actionProxy, "name", {
13162
- configurable: true,
13163
- get() {
13164
- return nameSignal.value;
13165
- },
13166
- });
13167
- Object.defineProperty(actionProxy, "callSource", {
13168
- configurable: true,
13169
- get() {
13170
- return callSourceSignal.value;
13171
- },
13172
- });
13173
- actionWeakMap.set(actionProxy, actionProxy);
13174
- }
13178
+ const actionProxy = function actionProxyFunction() {
13179
+ return actionProxy.rerun();
13180
+ };
13181
+ Object.defineProperty(actionProxy, "name", {
13182
+ configurable: true,
13183
+ get() {
13184
+ return nameSignal.value;
13185
+ },
13186
+ });
13187
+ Object.defineProperty(actionProxy, "callSource", {
13188
+ configurable: true,
13189
+ get() {
13190
+ return callSourceSignal.value;
13191
+ },
13192
+ });
13193
+ actionWeakMap.set(actionProxy, actionProxy);
13175
13194
 
13176
13195
  // Create our own signal for params that we control completely
13177
13196
  const proxyParamsSignal = signal(paramsSignal.value);
@@ -13374,11 +13393,6 @@ const isPlainObject$1 = (obj) => {
13374
13393
  );
13375
13394
  };
13376
13395
 
13377
- const COMPLETED_ACTION = createAction(() => undefined, {
13378
- name: "ACTION.COMPLETED",
13379
- });
13380
- getActionPrivateProperties(COMPLETED_ACTION).performRun({});
13381
-
13382
13396
  // used by form elements such as <input>, <select>, <textarea> to have their own action bound to a single parameter
13383
13397
  // when inside a <form> the form params are updated when the form element single param is updated
13384
13398
  const useActionBoundToOneParam = (action, paramsSignal) => {
@@ -13400,19 +13414,27 @@ const useAction = (action, paramsSignal) => {
13400
13414
  };
13401
13415
 
13402
13416
  const useBoundAction = (action, actionParamsSignal) => {
13403
- const actionRef = useRef();
13417
+ // The cache gives an inline function a stable action identity across renders.
13418
+ // That identity is only wanted while `action` stays the same kind
13419
+ // (function to function); when the kind changes — none ↔ function ↔ action
13420
+ // object — each branch clears the other kind's refs so the control picks up
13421
+ // its new role instead of the action it was born with.
13422
+ const noopActionRef = useRef();
13423
+ const actionFromFunctionRef = useRef();
13404
13424
  const actionCallbackRef = useRef();
13405
13425
 
13406
13426
  if (!action) {
13407
- const existingAction = actionRef.current;
13408
- if (existingAction) {
13409
- return existingAction;
13427
+ actionFromFunctionRef.current = undefined;
13428
+ actionCallbackRef.current = undefined;
13429
+ const existingNoopAction = noopActionRef.current;
13430
+ if (existingNoopAction) {
13431
+ return existingNoopAction;
13410
13432
  }
13411
13433
  const noopAction = createAction(() => {}, { params: undefined });
13412
13434
  const noopActionBound = actionParamsSignal
13413
13435
  ? noopAction.bindParams(actionParamsSignal)
13414
13436
  : noopAction;
13415
- actionRef.current = noopActionBound;
13437
+ noopActionRef.current = noopActionBound;
13416
13438
  return noopActionBound;
13417
13439
  }
13418
13440
  const isFunction = typeof action === "function";
@@ -13423,7 +13445,7 @@ const useBoundAction = (action, actionParamsSignal) => {
13423
13445
  }
13424
13446
  if (isFunctionButNotAnActionFunction(action)) {
13425
13447
  actionCallbackRef.current = action;
13426
- const existingAction = actionRef.current;
13448
+ const existingAction = actionFromFunctionRef.current;
13427
13449
  if (existingAction) {
13428
13450
  return existingAction;
13429
13451
  }
@@ -13439,14 +13461,16 @@ const useBoundAction = (action, actionParamsSignal) => {
13439
13461
  },
13440
13462
  );
13441
13463
  if (!actionParamsSignal) {
13442
- actionRef.current = actionFromFunction;
13464
+ actionFromFunctionRef.current = actionFromFunction;
13443
13465
  return actionFromFunction;
13444
13466
  }
13445
13467
  const actionBoundToParams =
13446
13468
  actionFromFunction.bindParams(actionParamsSignal);
13447
- actionRef.current = actionBoundToParams;
13469
+ actionFromFunctionRef.current = actionBoundToParams;
13448
13470
  return actionBoundToParams;
13449
13471
  }
13472
+ actionFromFunctionRef.current = undefined;
13473
+ actionCallbackRef.current = undefined;
13450
13474
  if (actionParamsSignal) {
13451
13475
  return action.bindParams(actionParamsSignal);
13452
13476
  }
@@ -18873,7 +18897,7 @@ const setupNetworkMonitoring = () => {
18873
18897
  };
18874
18898
  setupNetworkMonitoring();
18875
18899
 
18876
- installImportMetaCssBuild(import.meta);const css$11 = /* css */`
18900
+ installImportMetaCssBuild(import.meta);const css$12 = /* css */`
18877
18901
  .navi_loading_indicator_fluid_container {
18878
18902
  position: relative;
18879
18903
  display: flex;
@@ -18905,7 +18929,7 @@ const LoadingIndicatorFluid = ({
18905
18929
  visuallyHidden,
18906
18930
  ...rest
18907
18931
  }) => {
18908
- import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
18932
+ import.meta.css = [css$12, "@jsenv/navi/src/graphic/loading/loading_indicator_fluid.jsx"];
18909
18933
  const ref = useRef(null);
18910
18934
  // The container dimensions can be deduced from the ref itself as the indicator is absolute inset 0
18911
18935
  const [containerWidth, setContainerWidth] = useState(0);
@@ -19110,7 +19134,7 @@ const LoadingRectangleSvg = ({
19110
19134
  });
19111
19135
  };
19112
19136
 
19113
- installImportMetaCssBuild(import.meta);const css$10 = /* css */`
19137
+ installImportMetaCssBuild(import.meta);const css$11 = /* css */`
19114
19138
  .navi_loading_outline_wrapper {
19115
19139
  position: absolute;
19116
19140
  /* Controls place the outline slightly outside their box, right on top of
@@ -19147,7 +19171,7 @@ installImportMetaCssBuild(import.meta);const css$10 = /* css */`
19147
19171
  }
19148
19172
  `;
19149
19173
  const LoadingOutline = props => {
19150
- import.meta.css = [css$10, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
19174
+ import.meta.css = [css$11, "@jsenv/navi/src/graphic/loading/loading_outline.jsx"];
19151
19175
  if (props.containerRef) {
19152
19176
  const container = props.containerRef.current;
19153
19177
  if (!container) {
@@ -19437,7 +19461,7 @@ const selectByTextStrings = (element, range, startText, endText) => {
19437
19461
  };
19438
19462
 
19439
19463
  installImportMetaCssBuild(import.meta);// https://jsfiddle.net/v5xzJ/4/
19440
- const css$$ = /* css */`
19464
+ const css$10 = /* css */`
19441
19465
  @layer navi {
19442
19466
  .navi_text {
19443
19467
  &[data-skeleton] {
@@ -19958,7 +19982,7 @@ const TextShrinkWrap = props => {
19958
19982
  });
19959
19983
  };
19960
19984
  const TextUI = props => {
19961
- import.meta.css = [css$$, "@jsenv/navi/src/text/text.jsx"];
19985
+ import.meta.css = [css$10, "@jsenv/navi/src/text/text.jsx"];
19962
19986
  let {
19963
19987
  ref,
19964
19988
  spacing,
@@ -27333,18 +27357,31 @@ const useInteractiveProps = (props, {
27333
27357
  controlRootProps.onnavi_action_end?.(e);
27334
27358
  uiStateController.onActionEnd(e);
27335
27359
 
27336
- // For radio/checkbox: auto-trigger the parent group's action after the
27337
- // leaf action completes. The parent (radio_group/checkbox_group) has
27338
- // already aggregated the new state by now, so uiStateSignal is correct.
27360
+ // Auto-trigger the parent group's action after the leaf action
27361
+ // completes, for the groups that ARE one control made of parts: a
27362
+ // radio or checkbox group, a wheel group (an hour wheel settling is
27363
+ // the time settling). The parent has already aggregated the new
27364
+ // state by now, so uiStateSignal is correct. One level only: the
27365
+ // parent's own action end does not climb further unless that parent
27366
+ // is itself such a group.
27339
27367
  const parentController = uiStateController.parentUIStateController;
27340
- if (parentController && (parentController.controlType === "radio_group" || parentController.controlType === "checkbox_group")) {
27368
+ if (parentController && (parentController.controlType === "radio_group" || parentController.controlType === "checkbox_group" || parentController.controlType === "wheel_group")) {
27341
27369
  const parentEl = parentController.ref.current;
27342
27370
  if (parentEl) {
27343
- const originalEvent = e.detail.eventChain[0];
27344
27371
  dispatchRequestAction(parentEl, {
27345
- event: originalEvent,
27372
+ event: e.detail.eventChain[0],
27346
27373
  name: "auto_group_action",
27347
- requester: e.detail.requester
27374
+ requester: e.detail.requester,
27375
+ // The interactivity gate is not re-asked: the user already
27376
+ // interacted — with the child, whose gate said yes — and this
27377
+ // follow-up is automatic. Asking again would also answer
27378
+ // wrong: this event is dispatched inside the batch() that
27379
+ // settles the child's action, where a bound action still
27380
+ // READS as running (its state is mirrored through a signal
27381
+ // effect the batch defers, see watchActionCompletion) — the
27382
+ // busy constraint would refuse the group for an action that
27383
+ // is already over. The validity gate still applies.
27384
+ bypassInteractivity: true
27348
27385
  });
27349
27386
  }
27350
27387
  }
@@ -27406,7 +27443,7 @@ const getAssociatedLabels = element => {
27406
27443
  return Array.from(element.labels);
27407
27444
  };
27408
27445
 
27409
- installImportMetaCssBuild(import.meta);const css$_ = /* css */`
27446
+ installImportMetaCssBuild(import.meta);const css$$ = /* css */`
27410
27447
  @layer navi {
27411
27448
  .navi_button {
27412
27449
  --button-border-radius: var(--navi-control-border-radius);
@@ -27837,7 +27874,7 @@ installImportMetaCssBuild(import.meta);const css$_ = /* css */`
27837
27874
  }
27838
27875
  `;
27839
27876
  const ButtonUI = props => {
27840
- import.meta.css = [css$_, "@jsenv/navi/src/control/input/button_ui.jsx"];
27877
+ import.meta.css = [css$$, "@jsenv/navi/src/control/input/button_ui.jsx"];
27841
27878
  const {
27842
27879
  ref,
27843
27880
  // href/link
@@ -29548,7 +29585,7 @@ installImportMetaCssBuild(import.meta);/**
29548
29585
  * reaches the real container.
29549
29586
  */
29550
29587
  let openLocalDialogCount = 0;
29551
- const css$Z = /* css */`
29588
+ const css$_ = /* css */`
29552
29589
  @layer navi {
29553
29590
  .navi_dialog {
29554
29591
  /* Min gap between the dialog and the edges of its container. Written
@@ -30132,7 +30169,7 @@ const css$Z = /* css */`
30132
30169
  * @param {import("ignore:preact").ComponentChildren} props.children
30133
30170
  */
30134
30171
  const Dialog = props => {
30135
- import.meta.css = [css$Z, "@jsenv/navi/src/layout/dialog.jsx"];
30172
+ import.meta.css = [css$_, "@jsenv/navi/src/layout/dialog.jsx"];
30136
30173
  if (props.openController) {
30137
30174
  return jsx(ControlledDialog, {
30138
30175
  ...props
@@ -31124,7 +31161,7 @@ installImportMetaCssBuild(import.meta);/**
31124
31161
  * and applied.
31125
31162
  */
31126
31163
  let openLocalPopoverCount = 0;
31127
- const css$Y = /* css */`
31164
+ const css$Z = /* css */`
31128
31165
  @layer navi {
31129
31166
  .navi_popover {
31130
31167
  /* soft: user-configurable preferred max-height. Kept as a *default*
@@ -31580,7 +31617,7 @@ const css$Y = /* css */`
31580
31617
  * @param {import("ignore:preact").ComponentChildren} props.children
31581
31618
  */
31582
31619
  const Popover = props => {
31583
- import.meta.css = [css$Y, "@jsenv/navi/src/layout/popover.jsx"];
31620
+ import.meta.css = [css$Z, "@jsenv/navi/src/layout/popover.jsx"];
31584
31621
  if (props.openController) {
31585
31622
  return jsx(ControlledPopover, {
31586
31623
  ...props
@@ -32608,7 +32645,7 @@ installImportMetaCssBuild(import.meta);/**
32608
32645
  * event, and a caller replacing the body entirely then has one protocol to
32609
32646
  * follow — `--navi-confirm` for yes, anything that closes for no.
32610
32647
  */
32611
- const css$X = /* css */`
32648
+ const css$Y = /* css */`
32612
32649
  /* The width lives on the body rather than on the popup, so that custom
32613
32650
  content (which replaces this body entirely) sizes itself instead of
32614
32651
  inheriting a ceiling meant for a sentence-long question. */
@@ -32745,7 +32782,7 @@ const ConfirmPopup = ({
32745
32782
  onAnswer,
32746
32783
  onClosed
32747
32784
  }) => {
32748
- import.meta.css = [css$X, "@jsenv/navi/src/action/confirm_popup.jsx"];
32785
+ import.meta.css = [css$Y, "@jsenv/navi/src/action/confirm_popup.jsx"];
32749
32786
  const {
32750
32787
  mode,
32751
32788
  confirmLabel,
@@ -32829,7 +32866,7 @@ const defaultBody = (message, {
32829
32866
  });
32830
32867
  };
32831
32868
 
32832
- installImportMetaCssBuild(import.meta);const css$W = /* css */`
32869
+ installImportMetaCssBuild(import.meta);const css$X = /* css */`
32833
32870
  .action_error {
32834
32871
  margin-top: 0;
32835
32872
  margin-bottom: 20px;
@@ -32854,7 +32891,7 @@ const ActionRenderer = ({
32854
32891
  children,
32855
32892
  disabled
32856
32893
  }) => {
32857
- import.meta.css = [css$W, "@jsenv/navi/src/action/action_renderer.jsx"];
32894
+ import.meta.css = [css$X, "@jsenv/navi/src/action/action_renderer.jsx"];
32858
32895
  if (action === undefined) {
32859
32896
  throw new Error("ActionRenderer requires an action to render, but none was provided.");
32860
32897
  }
@@ -34579,7 +34616,7 @@ const describeRangeAsked = (rangeParams) => {
34579
34616
  };
34580
34617
 
34581
34618
  const resourceLifecycleManager = createResourceLifecycleManager();
34582
- const debug$2 = (args) => {
34619
+ const debug$2 = (...args) => {
34583
34620
  {
34584
34621
  return;
34585
34622
  }
@@ -34644,6 +34681,7 @@ const resource = (
34644
34681
  DELETE_MANY,
34645
34682
  } = {},
34646
34683
  ) => {
34684
+ const declarationSite = getDeclarationSite();
34647
34685
  if (idKey === undefined) {
34648
34686
  idKey = uniqueKeys.length === 0 ? "id" : uniqueKeys[0];
34649
34687
  }
@@ -34692,6 +34730,7 @@ const resource = (
34692
34730
  const createRestActionForRoot = createRestActionFactoryForRoot(name, {
34693
34731
  idKey,
34694
34732
  store,
34733
+ declarationSite,
34695
34734
  });
34696
34735
  return createResource(name, {
34697
34736
  idKey,
@@ -34730,11 +34769,8 @@ const createResource = (
34730
34769
  paramScope,
34731
34770
  rerunOn,
34732
34771
  dependencies,
34733
- } = {},
34772
+ },
34734
34773
  ) => {
34735
- if (idKey === undefined) {
34736
- idKey = uniqueKeys.length === 0 ? "id" : uniqueKeys[0];
34737
- }
34738
34774
  const params = paramScope.params;
34739
34775
  const stateFacade = {
34740
34776
  // public
@@ -34755,7 +34791,6 @@ const createResource = (
34755
34791
  store,
34756
34792
  addItemSetup,
34757
34793
  };
34758
- const lifecycleCtx = { onComplete: null };
34759
34794
 
34760
34795
  resourceLifecycleManager.registerResource(stateFacade, {
34761
34796
  rerunOn,
@@ -34763,7 +34798,7 @@ const createResource = (
34763
34798
  dependencies,
34764
34799
  uniqueKeys,
34765
34800
  });
34766
- lifecycleCtx.onComplete = (actionCompleted) => {
34801
+ const onActionComplete = (actionCompleted) => {
34767
34802
  resourceLifecycleManager.onActionComplete(actionCompleted, {
34768
34803
  resourceScope: stateFacade,
34769
34804
  });
@@ -34800,6 +34835,7 @@ const createResource = (
34800
34835
  paramsToInject,
34801
34836
  { dependencies: withParamsDeps, rerunOn: withParamsRerunOn } = {},
34802
34837
  ) => {
34838
+ const declarationSite = getDeclarationSite();
34803
34839
  if (!paramsToInject || Object.keys(paramsToInject).length === 0) {
34804
34840
  throw new Error(`resource(${name}).withParams() requires parameters`);
34805
34841
  }
@@ -34810,6 +34846,7 @@ const createResource = (
34810
34846
  const createRestActionWithParams = createRestActionFactoryForRoot(name, {
34811
34847
  idKey,
34812
34848
  store,
34849
+ declarationSite,
34813
34850
  });
34814
34851
  return createResource(name, {
34815
34852
  idKey,
@@ -34862,40 +34899,37 @@ const createResource = (
34862
34899
  DELETE,
34863
34900
  } = {},
34864
34901
  ) => {
34902
+ const declarationSite = getDeclarationSite();
34865
34903
  const childName = `${name}.${propertyName}`;
34904
+ const childIdKey = childResource.idKey;
34905
+ const childStore = childResource.store;
34866
34906
  addItemSetup((item) => {
34867
- const childIdKeyForSetup = childResource.idKey;
34868
34907
  const childItemIdSignal = signal();
34869
34908
  const updateChildItemId = (value) => {
34870
34909
  const currentChildItemId = childItemIdSignal.peek();
34910
+ let childItemProps;
34871
34911
  if (isProps(value)) {
34872
- const childItem = childResource.store.upsert(value);
34873
- const childItemId = childItem[childIdKeyForSetup];
34874
- if (currentChildItemId === childItemId) {
34875
- return false;
34876
- }
34877
- childItemIdSignal.value = childItemId;
34878
- return true;
34879
- }
34880
- if (primitiveCanBeId(value)) {
34881
- const childItemProps = { [childIdKeyForSetup]: value };
34882
- const childItem = childResource.store.upsert(childItemProps);
34883
- const childItemId = childItem[childIdKeyForSetup];
34884
- if (currentChildItemId === childItemId) {
34912
+ childItemProps = value;
34913
+ } else if (primitiveCanBeId(value)) {
34914
+ childItemProps = { [childIdKey]: value };
34915
+ } else {
34916
+ if (currentChildItemId === undefined) {
34885
34917
  return false;
34886
34918
  }
34887
- childItemIdSignal.value = childItemId;
34919
+ childItemIdSignal.value = undefined;
34888
34920
  return true;
34889
34921
  }
34890
- if (currentChildItemId === undefined) {
34922
+ const childItem = childStore.upsert(childItemProps);
34923
+ const childItemId = childItem[childIdKey];
34924
+ if (currentChildItemId === childItemId) {
34891
34925
  return false;
34892
34926
  }
34893
- childItemIdSignal.value = undefined;
34927
+ childItemIdSignal.value = childItemId;
34894
34928
  return true;
34895
34929
  };
34896
34930
  updateChildItemId(item[propertyName]);
34897
34931
  const childItemSignal = computed(() =>
34898
- childResource.store.select(childItemIdSignal.value),
34932
+ childStore.select(childItemIdSignal.value),
34899
34933
  );
34900
34934
  const childItemFacadeSignal = computed(() => {
34901
34935
  const childItem = childItemSignal.value;
@@ -34926,9 +34960,7 @@ const createResource = (
34926
34960
  );
34927
34961
  });
34928
34962
 
34929
- const childIdKey = childResource.idKey;
34930
- const childStore = childResource.store;
34931
- const createRestActionForOne = (verb, callback, { lifecycleCtx }) => {
34963
+ const createRestActionForOne = (verb, callback, { onActionComplete }) => {
34932
34964
  const applyResultToValue =
34933
34965
  verb === "DELETE"
34934
34966
  ? (itemId) => {
@@ -34940,33 +34972,18 @@ const createResource = (
34940
34972
  });
34941
34973
  return childItemId;
34942
34974
  }
34943
- : // callback must return object with the following format:
34944
- // {
34945
- // [idKey]: 123,
34946
- // [propertyName]: {
34947
- // [childIdKey]: 456, ...childProps
34948
- // }
34949
- // }
34950
- // the following could happen too if there is no relationship
34951
- // {
34952
- // [idKey]: 123,
34953
- // [propertyName]: null
34954
- // }
34975
+ : // GET/PUT contract (see .one() JSDoc): the parent object with the
34976
+ // relationship nested inside, or null for no relationship.
34955
34977
  (result) => {
34956
34978
  const item = store.upsert(result);
34957
34979
  const childItem = item[propertyName];
34958
- const childItemId = childItem ? childItem[childIdKey] : undefined;
34959
- return childItemId;
34980
+ return childItem ? childItem[childIdKey] : undefined;
34960
34981
  };
34961
-
34962
- const callerInfo = getCallerInfo(null, 2);
34963
- const locationInfo =
34964
- callerInfo.file && callerInfo.line && callerInfo.column
34965
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
34966
- : callerInfo.raw || "unknown location";
34967
- const originalActionName = `${name}.${verb}`;
34968
-
34969
- const actionAffectingOneItem = createAction(callback, {
34982
+ const throwInvalidResult = createInvalidResultThrower(
34983
+ `${name}.${verb}`,
34984
+ declarationSite,
34985
+ );
34986
+ return createAction(callback, {
34970
34987
  meta: {
34971
34988
  verb,
34972
34989
  isMany: false,
@@ -34974,35 +34991,30 @@ const createResource = (
34974
34991
  },
34975
34992
  name: `${name}.${verb}`,
34976
34993
  resultToValue: (result, action) => {
34977
- const actionLabel = action.name;
34978
-
34979
34994
  if (verb === "DELETE") {
34980
34995
  if (!isProps(result) && !primitiveCanBeId(result)) {
34981
- throw new TypeError(
34982
- `${actionLabel} must return an object (that will be used to drop "${name}" resource), received ${result}.
34983
- ${originalActionName} source location: ${locationInfo}`,
34996
+ throwInvalidResult(
34997
+ action.name,
34998
+ `an object (that will be used to drop "${name}" resource)`,
34999
+ result,
34984
35000
  );
34985
35001
  }
34986
- return applyResultToValue(result);
34987
- }
34988
- if (!isProps(result)) {
34989
- throw new TypeError(
34990
- `${actionLabel} must return an object (that will be used to upsert "${name}" resource), received ${result}.
34991
- ${originalActionName} source location: ${locationInfo}`,
35002
+ } else if (!isProps(result)) {
35003
+ throwInvalidResult(
35004
+ action.name,
35005
+ `an object (that will be used to upsert "${name}" resource)`,
35006
+ result,
34992
35007
  );
34993
35008
  }
34994
35009
  return applyResultToValue(result);
34995
35010
  },
34996
35011
  valueToData: (childItemId) => childStore.select(childItemId),
34997
- completeSideEffect: (actionCompleted) => {
34998
- lifecycleCtx.onComplete(actionCompleted);
34999
- },
35012
+ completeSideEffect: onActionComplete,
35000
35013
  });
35001
- return actionAffectingOneItem;
35002
35014
  };
35003
35015
 
35004
35016
  return createResource(childName, {
35005
- idKey: childResource.idKey,
35017
+ idKey: childIdKey,
35006
35018
  restCallbacks: {
35007
35019
  GET,
35008
35020
  PUT,
@@ -35059,49 +35071,21 @@ ${originalActionName} source location: ${locationInfo}`,
35059
35071
  DELETE_MANY,
35060
35072
  } = {},
35061
35073
  ) => {
35074
+ const declarationSite = getDeclarationSite();
35062
35075
  const childStore = childResource.store;
35063
35076
  const childIdKey = childResource.idKey;
35064
35077
  const childName = `${name}.${propertyName}`;
35065
35078
  addItemSetup((item) => {
35066
35079
  const childItemIdArraySignal = signal([]);
35067
- const updateChildItemIdArray = (valueArray) => {
35068
- const currentIdArray = childItemIdArraySignal.peek();
35069
- if (!Array.isArray(valueArray)) {
35070
- if (currentIdArray.length === 0) return;
35071
- childItemIdArraySignal.value = [];
35072
- return;
35073
- }
35074
- let i = 0;
35075
- const idArray = [];
35076
- let modified = false;
35077
- while (i < valueArray.length) {
35078
- const value = valueArray[i];
35079
- const currentIdAtIndex = currentIdArray[idArray.length];
35080
- i++;
35081
- if (isProps(value)) {
35082
- const childItem = childResource.store.upsert(value);
35083
- const childItemId = childItem[childIdKey];
35084
- if (currentIdAtIndex !== childItemId) modified = true;
35085
- idArray.push(childItemId);
35086
- continue;
35087
- }
35088
- if (primitiveCanBeId(value)) {
35089
- const childItemProps = { [childIdKey]: value };
35090
- const childItem = childResource.store.upsert(childItemProps);
35091
- const childItemId = childItem[childIdKey];
35092
- if (currentIdAtIndex !== childItemId) modified = true;
35093
- idArray.push(childItemId);
35094
- continue;
35095
- }
35096
- }
35097
- if (modified || currentIdArray.length !== idArray.length) {
35098
- childItemIdArraySignal.value = idArray;
35099
- }
35100
- };
35080
+ const updateChildItemIdArray = createChildIdArrayUpdater(
35081
+ childStore,
35082
+ childIdKey,
35083
+ childItemIdArraySignal,
35084
+ );
35101
35085
  updateChildItemIdArray(item[propertyName]);
35102
35086
  const childItemArraySignal = computed(() => {
35103
35087
  const idArray = childItemIdArraySignal.value;
35104
- const arr = childResource.store.selectAll(idArray);
35088
+ const arr = childStore.selectAll(idArray);
35105
35089
  Object.defineProperty(arr, SYMBOL_OBJECT_SIGNAL, {
35106
35090
  value: childItemArraySignal,
35107
35091
  writable: false,
@@ -35114,23 +35098,27 @@ ${originalActionName} source location: ${locationInfo}`,
35114
35098
  get: () => childItemArraySignal.value,
35115
35099
  set: updateChildItemIdArray,
35116
35100
  });
35117
- syncIdArrayOnRename(
35118
- childResource.store,
35119
- childIdKey,
35120
- childItemIdArraySignal,
35121
- );
35101
+ syncIdArrayOnRename(childStore, childIdKey, childItemIdArraySignal);
35122
35102
  });
35123
35103
  const createRestActionForMany = (
35124
35104
  verb,
35125
35105
  callback,
35126
- { isMany, lifecycleCtx },
35106
+ { isMany, onActionComplete },
35127
35107
  ) => {
35128
35108
  if (!isMany) {
35129
- return createRestActionAffectingOneItem(verb, callback, lifecycleCtx);
35109
+ return createRestActionAffectingOneItem(verb, callback, {
35110
+ onActionComplete,
35111
+ });
35130
35112
  }
35131
- return createRestActionAffectingManyItems(verb, callback, lifecycleCtx);
35113
+ return createRestActionAffectingManyItems(verb, callback, {
35114
+ onActionComplete,
35115
+ });
35132
35116
  };
35133
- const createRestActionAffectingOneItem = (verb, callback, lifecycleCtx) => {
35117
+ const createRestActionAffectingOneItem = (
35118
+ verb,
35119
+ callback,
35120
+ { onActionComplete },
35121
+ ) => {
35134
35122
  const applyResultToValue =
35135
35123
  verb === "DELETE"
35136
35124
  ? ([itemId, childItemId]) => {
@@ -35139,8 +35127,7 @@ ${originalActionName} source location: ${locationInfo}`,
35139
35127
  const childItemArrayWithoutThisOne = [];
35140
35128
  let found = false;
35141
35129
  for (const childItemCandidate of childItemArray) {
35142
- const childItemCandidateId = childItemCandidate[childIdKey];
35143
- if (childItemCandidateId === childItemId) {
35130
+ if (childItemCandidate[childIdKey] === childItemId) {
35144
35131
  found = true;
35145
35132
  } else {
35146
35133
  childItemArrayWithoutThisOne.push(childItemCandidate);
@@ -35155,162 +35142,127 @@ ${originalActionName} source location: ${locationInfo}`,
35155
35142
  return childItemId;
35156
35143
  }
35157
35144
  : (childData) => {
35145
+ // an array is [property, value, props], used to rename the child id
35158
35146
  const childItem = Array.isArray(childData)
35159
35147
  ? childStore.upsert(...childData)
35160
35148
  : childStore.upsert(childData);
35161
- const childItemId = childItem[childIdKey];
35162
- return childItemId;
35149
+ return childItem[childIdKey];
35163
35150
  };
35164
-
35165
- const callerInfo = getCallerInfo(null, 2);
35166
- const locationInfo =
35167
- callerInfo.file && callerInfo.line && callerInfo.column
35168
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
35169
- : callerInfo.raw || "unknown location";
35170
- const originalActionName = `${name}.${verb}`;
35171
-
35172
- const actionAffectingOneItem = createAction(callback, {
35151
+ const throwInvalidResult = createInvalidResultThrower(
35152
+ `${name}.${verb}`,
35153
+ declarationSite,
35154
+ );
35155
+ return createAction(callback, {
35173
35156
  meta: { verb, isMany: false, paramScope },
35174
35157
  name: `${name}.${verb}`,
35175
35158
  resultToValue: (result, action) => {
35176
- const actionLabel = action.name;
35177
-
35178
35159
  if (verb === "DELETE") {
35179
35160
  if (!Array.isArray(result) || result.length !== 2) {
35180
- throw new TypeError(
35181
- `${actionLabel} must return an array [itemId, childItemId] (that will be used to remove relationship), received ${result}.
35182
- ${originalActionName} source location: ${locationInfo}`,
35161
+ throwInvalidResult(
35162
+ action.name,
35163
+ `an array [itemId, childItemId] (that will be used to remove relationship)`,
35164
+ result,
35183
35165
  );
35184
35166
  }
35185
- return applyResultToValue(result);
35186
- }
35187
- if (!isProps(result)) {
35188
- throw new TypeError(
35189
- `${actionLabel} must return an object (that will be used to upsert child item), received ${result}.
35190
- ${originalActionName} source location: ${locationInfo}`,
35167
+ } else if (!isProps(result)) {
35168
+ throwInvalidResult(
35169
+ action.name,
35170
+ `an object (that will be used to upsert child item)`,
35171
+ result,
35191
35172
  );
35192
35173
  }
35193
35174
  return applyResultToValue(result);
35194
35175
  },
35195
35176
  valueToData: (childItemId) => childStore.select(childItemId),
35196
- completeSideEffect: (actionCompleted) => {
35197
- lifecycleCtx.onComplete(actionCompleted);
35198
- },
35177
+ completeSideEffect: onActionComplete,
35199
35178
  });
35200
- return actionAffectingOneItem;
35201
35179
  };
35202
35180
  const createRestActionAffectingManyItems = (
35203
35181
  verb,
35204
35182
  callback,
35205
- lifecycleCtx,
35183
+ { onActionComplete },
35206
35184
  ) => {
35207
35185
  const applyResultToValue =
35208
35186
  verb === "GET"
35209
35187
  ? (result) => {
35210
- // callback must return object with the following format:
35211
- // {
35212
- // [idKey]: 123,
35213
- // [propertyName]: [
35214
- // { [childIdKey]: 456, ...childProps },
35215
- // { [childIdKey]: 789, ...childProps },
35216
- // ...
35217
- // ]
35218
- // }
35219
- // the array can be empty
35188
+ // GET_MANY contract (see .many() JSDoc): the parent object with
35189
+ // the child array nested inside; the array replaces the relationship.
35220
35190
  const item = store.upsert(result);
35221
35191
  const childItemArray = item[propertyName];
35222
- const childItemIdArray = childItemArray.map(
35223
- (childItem) => childItem[childIdKey],
35224
- );
35225
- return childItemIdArray;
35192
+ return childItemArray.map((childItem) => childItem[childIdKey]);
35226
35193
  }
35227
35194
  : verb === "DELETE"
35228
35195
  ? ([itemIdOrMutableId, childItemIdOrMutableIdArray]) => {
35229
35196
  const item = store.select(itemIdOrMutableId);
35230
35197
  const childItemArray = item[propertyName];
35231
- const deletedChildItemIdArray = [];
35232
- const childItemArrayWithoutThoose = [];
35233
- let someFound = false;
35234
- const deletedChildItemArray = childStore.select(
35235
- childItemIdOrMutableIdArray,
35198
+ const deletedChildItemSet = new Set(
35199
+ childStore.selectAll(childItemIdOrMutableIdArray),
35236
35200
  );
35201
+ const deletedChildItemIdArray = [];
35202
+ const childItemArrayWithoutThose = [];
35237
35203
  for (const childItemCandidate of childItemArray) {
35238
- if (deletedChildItemArray.includes(childItemCandidate)) {
35239
- someFound = true;
35204
+ if (deletedChildItemSet.has(childItemCandidate)) {
35240
35205
  deletedChildItemIdArray.push(
35241
35206
  childItemCandidate[childIdKey],
35242
35207
  );
35243
35208
  } else {
35244
- childItemArrayWithoutThoose.push(childItemCandidate);
35209
+ childItemArrayWithoutThose.push(childItemCandidate);
35245
35210
  }
35246
35211
  }
35247
- if (someFound) {
35212
+ if (deletedChildItemIdArray.length > 0) {
35248
35213
  store.upsert({
35249
35214
  [idKey]: item[idKey],
35250
- [propertyName]: childItemArrayWithoutThoose,
35215
+ [propertyName]: childItemArrayWithoutThose,
35251
35216
  });
35252
35217
  }
35253
35218
  return deletedChildItemIdArray;
35254
35219
  }
35255
35220
  : (childDataArray) => {
35256
35221
  const childItemArray = childStore.upsert(childDataArray);
35257
- const childItemIdArray = childItemArray.map(
35258
- (childItem) => childItem[childIdKey],
35259
- );
35260
- return childItemIdArray;
35222
+ return childItemArray.map((childItem) => childItem[childIdKey]);
35261
35223
  };
35262
-
35263
- const callerInfo = getCallerInfo(null, 2);
35264
- const locationInfo =
35265
- callerInfo.file && callerInfo.line && callerInfo.column
35266
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
35267
- : callerInfo.raw || "unknown location";
35268
- const originalActionName = `${name}.${verb}[many]`;
35269
-
35270
- const actionAffectingManyItem = createAction(callback, {
35224
+ const throwInvalidResult = createInvalidResultThrower(
35225
+ `${name}.${verb}[many]`,
35226
+ declarationSite,
35227
+ );
35228
+ return createAction(callback, {
35271
35229
  meta: { verb, isMany: true, paramScope },
35272
35230
  name: `${name}.${verb}[many]`,
35273
35231
  dataDefault: [],
35274
35232
  resultToValue: (result, action) => {
35275
- const actionLabel = action.name;
35276
-
35277
35233
  if (verb === "GET") {
35278
35234
  if (!isProps(result)) {
35279
- throw new TypeError(
35280
- `${actionLabel} must return an object (that will be used to upsert "${name}" resource with many relationships), received ${result}.
35281
- ${originalActionName} source location: ${locationInfo}`,
35235
+ throwInvalidResult(
35236
+ action.name,
35237
+ `an object (that will be used to upsert "${name}" resource with many relationships)`,
35238
+ result,
35282
35239
  );
35283
35240
  }
35284
- return applyResultToValue(result);
35285
- }
35286
- if (verb === "DELETE") {
35241
+ } else if (verb === "DELETE") {
35287
35242
  if (
35288
35243
  !Array.isArray(result) ||
35289
35244
  result.length !== 2 ||
35290
35245
  !Array.isArray(result[1])
35291
35246
  ) {
35292
- throw new TypeError(
35293
- `${actionLabel} must return an array [itemId, childItemIdArray] (that will be used to remove relationships), received ${result}.
35294
- ${originalActionName} source location: ${locationInfo}`,
35247
+ throwInvalidResult(
35248
+ action.name,
35249
+ `an array [itemId, childItemIdArray] (that will be used to remove relationships)`,
35250
+ result,
35295
35251
  );
35296
35252
  }
35297
- return applyResultToValue(result);
35298
- }
35299
- if (!Array.isArray(result)) {
35300
- throw new TypeError(
35301
- `${actionLabel} must return an array of objects (that will be used to upsert child items), received ${result}.
35302
- ${originalActionName} source location: ${locationInfo}`,
35253
+ } else if (!Array.isArray(result)) {
35254
+ throwInvalidResult(
35255
+ action.name,
35256
+ `an array of objects (that will be used to upsert child items)`,
35257
+ result,
35303
35258
  );
35304
35259
  }
35305
35260
  return applyResultToValue(result);
35306
35261
  },
35307
35262
  valueToData: (childItemIdArray) =>
35308
35263
  childStore.selectAll(childItemIdArray),
35309
- completeSideEffect: (actionCompleted) => {
35310
- lifecycleCtx.onComplete(actionCompleted);
35311
- },
35264
+ completeSideEffect: onActionComplete,
35312
35265
  });
35313
- return actionAffectingManyItem;
35314
35266
  };
35315
35267
 
35316
35268
  return createResource(childName, {
@@ -35375,26 +35327,20 @@ ${originalActionName} source location: ${locationInfo}`,
35375
35327
  ) => {
35376
35328
  const childName = `${name}.${propertyName}`;
35377
35329
 
35378
- // setupCallbackSet: callbacks added by chained .one()/.many()
35379
- // Applied to each per-scope child item object when it is first created.
35380
- const childItemSetupCallbackSet = new Set();
35381
- const childAddItemSetup = (callback) =>
35382
- childItemSetupCallbackSet.add(callback);
35383
- const scopedItemMap = new Map(); // ownerId → stable child item object
35384
- const scopedSignalMap = new Map(); // ownerId → signal<childItem | null>
35330
+ // Callbacks added by chained .one()/.many() on the child resource,
35331
+ // applied to each per-owner child object when it is first created.
35332
+ const childSetupCallbackSet = new Set();
35333
+ const childAddItemSetup = (callback) => childSetupCallbackSet.add(callback);
35334
+ const applyPropsMap = new Map(); // ownerId → applyProps(props | null)
35385
35335
  addItemSetup((ownerItem) => {
35386
35336
  const ownerId = ownerItem[idKey];
35387
- // Create a stable child item mutated in place via applyProps.
35388
- // Reactive getters/setters from chained .one() etc. are defined on this object now
35389
- // so they survive across multiple prop updates.
35337
+ // A stable child object, mutated in place: reactive getters/setters from
35338
+ // chained .one() etc. are defined on it once and survive prop updates.
35390
35339
  const childItem = {};
35391
- for (const childSetup of childItemSetupCallbackSet) {
35340
+ for (const childSetup of childSetupCallbackSet) {
35392
35341
  childSetup(childItem);
35393
35342
  }
35394
- scopedItemMap.set(ownerId, childItem);
35395
35343
  const childSignal = signal(null);
35396
- scopedSignalMap.set(ownerId, childSignal);
35397
-
35398
35344
  const applyProps = (props) => {
35399
35345
  if (!props) {
35400
35346
  childSignal.value = null;
@@ -35408,6 +35354,7 @@ ${originalActionName} source location: ${locationInfo}`,
35408
35354
  childSignal.value = childItem; // first activation: null → childItem
35409
35355
  }
35410
35356
  };
35357
+ applyPropsMap.set(ownerId, applyProps);
35411
35358
 
35412
35359
  applyProps(ownerItem[propertyName]);
35413
35360
 
@@ -35416,9 +35363,13 @@ ${originalActionName} source location: ${locationInfo}`,
35416
35363
  set: applyProps,
35417
35364
  });
35418
35365
  });
35419
- const createRestActionForScopedOne = (verb, callback, { lifecycleCtx }) => {
35366
+ const createRestActionForScopedOne = (
35367
+ verb,
35368
+ callback,
35369
+ { onActionComplete },
35370
+ ) => {
35420
35371
  const childActionName = `${childName}.${verb}`;
35421
- const restAction = createAction(callback, {
35372
+ return createAction(callback, {
35422
35373
  name: childActionName,
35423
35374
  meta: { verb, isMany: false, paramScope },
35424
35375
  resultToValue: (result) => {
@@ -35435,33 +35386,20 @@ ${originalActionName} source location: ${locationInfo}`,
35435
35386
  uniqueKeys,
35436
35387
  childActionName,
35437
35388
  );
35438
- const childItem = scopedItemMap.get(ownerId);
35439
- if (!childItem) {
35389
+ const applyProps = applyPropsMap.get(ownerId);
35390
+ if (!applyProps) {
35440
35391
  throw new Error(
35441
35392
  `${childActionName}: no item found for scope id "${ownerId}"`,
35442
35393
  );
35443
35394
  }
35444
- const childSignal = scopedSignalMap.get(ownerId);
35445
- if (props) {
35446
- for (const [key, value] of Object.entries(props)) {
35447
- childItem[key] = value;
35448
- }
35449
- if (childSignal.peek() !== childItem) {
35450
- childSignal.value = childItem;
35451
- }
35452
- } else {
35453
- childSignal.value = null;
35454
- }
35395
+ applyProps(props);
35455
35396
  return [ownerId, props];
35456
35397
  },
35457
- completeSideEffect: (actionCompleted) => {
35458
- lifecycleCtx.onComplete(actionCompleted);
35459
- },
35398
+ completeSideEffect: onActionComplete,
35460
35399
  });
35461
- return restAction;
35462
35400
  };
35463
35401
 
35464
- const childResource = createResource(childName, {
35402
+ return createResource(childName, {
35465
35403
  idKey: childIdKey,
35466
35404
  restCallbacks: {
35467
35405
  GET,
@@ -35477,7 +35415,6 @@ ${originalActionName} source location: ${locationInfo}`,
35477
35415
  rerunOn: scopedOneRerunOn ?? rerunOn,
35478
35416
  dependencies: scopedOneDependencies ?? dependencies,
35479
35417
  });
35480
- return childResource;
35481
35418
  };
35482
35419
 
35483
35420
  /**
@@ -35531,108 +35468,76 @@ ${originalActionName} source location: ${locationInfo}`,
35531
35468
  ) => {
35532
35469
  const childName = `${name}.${propertyName}`;
35533
35470
 
35534
- // setupCallbackSet: callbacks added by chained .one()/.many()
35535
- // Applied to each child item when it is created in a per-scope store.
35471
+ // Callbacks added by chained .one()/.many() on the child resource,
35472
+ // applied to each child item created in a per-owner store.
35536
35473
  const childSetupCallbackSet = new Set();
35537
35474
  const childAddItemSetup = (callback) => childSetupCallbackSet.add(callback);
35538
- const scopedStoreMap = new Map(); // ownerId → childStore
35539
- const scopedIdArraySignalMap = new Map(); // ownerId childItemIdArraySignal
35475
+ // ownerKey (id or any uniqueKey value){ childStore, idArraySignal }
35476
+ // One owner can be registered under several keys, all pointing to the same scope.
35477
+ const scopeMap = new Map();
35478
+ const createScope = (ownerKey) => {
35479
+ const childStore = arraySignalStore([], childIdKey, {
35480
+ name: `${childName}#${ownerKey} store`,
35481
+ createItem: (props) => {
35482
+ const childItem = {};
35483
+ Object.assign(childItem, props);
35484
+ for (const childSetup of childSetupCallbackSet) {
35485
+ childSetup(childItem);
35486
+ }
35487
+ return childItem;
35488
+ },
35489
+ });
35490
+ const scope = { childStore, idArraySignal: signal([]) };
35491
+ scopeMap.set(ownerKey, scope);
35492
+ return scope;
35493
+ };
35540
35494
  addItemSetup((item) => {
35541
35495
  const ownerId = item[idKey];
35542
35496
 
35543
- // Reuse an existing scoped store if one was already created via a uniqueKey
35544
- // (e.g. rows were fetched by tablename before the full table was loaded).
35545
- let childStore = scopedStoreMap.get(ownerId);
35546
- let childItemIdArraySignal = scopedIdArraySignalMap.get(ownerId);
35547
- if (!childStore) {
35497
+ // Reuse an existing scope if one was already created under a uniqueKey
35498
+ // value (e.g. rows were fetched by tablename before the full table was loaded).
35499
+ let scope = scopeMap.get(ownerId);
35500
+ if (!scope) {
35548
35501
  for (const uniqueKey of uniqueKeys) {
35549
35502
  const uniqueKeyValue = item[uniqueKey];
35550
- if (uniqueKeyValue !== undefined) {
35551
- const existing = scopedStoreMap.get(uniqueKeyValue);
35552
- if (existing) {
35553
- childStore = existing;
35554
- childItemIdArraySignal =
35555
- scopedIdArraySignalMap.get(uniqueKeyValue);
35556
- break;
35557
- }
35503
+ if (uniqueKeyValue !== undefined && scopeMap.has(uniqueKeyValue)) {
35504
+ scope = scopeMap.get(uniqueKeyValue);
35505
+ break;
35558
35506
  }
35559
35507
  }
35560
35508
  }
35561
- if (!childStore) {
35562
- childStore = arraySignalStore([], childIdKey, {
35563
- name: `${childName}#${ownerId} store`,
35564
- createItem: (props) => {
35565
- const childItem = {};
35566
- Object.assign(childItem, props);
35567
- for (const childSetup of childSetupCallbackSet) {
35568
- childSetup(childItem);
35569
- }
35570
- return childItem;
35571
- },
35572
- });
35573
- childItemIdArraySignal = signal([]);
35509
+ if (!scope) {
35510
+ scope = createScope(ownerId);
35574
35511
  }
35575
- scopedStoreMap.set(ownerId, childStore);
35576
- // Also register by each uniqueKey value so that resolveOwnerId works
35577
- // when a callback returns { [uniqueKey]: value } before the full item is loaded.
35512
+ // Register the scope under the id and every uniqueKey value so that
35513
+ // resolveOwnerId can address it whichever key a callback returns.
35514
+ scopeMap.set(ownerId, scope);
35578
35515
  for (const uniqueKey of uniqueKeys) {
35579
35516
  const uniqueKeyValue = item[uniqueKey];
35580
35517
  if (uniqueKeyValue !== undefined) {
35581
- scopedStoreMap.set(uniqueKeyValue, childStore);
35518
+ scopeMap.set(uniqueKeyValue, scope);
35582
35519
  }
35583
35520
  }
35584
35521
 
35585
- scopedIdArraySignalMap.set(ownerId, childItemIdArraySignal);
35586
- for (const uniqueKey of uniqueKeys) {
35587
- const uniqueKeyValue = item[uniqueKey];
35588
- if (uniqueKeyValue !== undefined) {
35589
- scopedIdArraySignalMap.set(uniqueKeyValue, childItemIdArraySignal);
35590
- }
35522
+ const { childStore, idArraySignal } = scope;
35523
+ const updateChildItemIdArray = createChildIdArrayUpdater(
35524
+ childStore,
35525
+ childIdKey,
35526
+ idArraySignal,
35527
+ );
35528
+ // The parent may not carry the property at all (e.g. created by a POST
35529
+ // that does not embed it): leave the collection of a reused scope
35530
+ // untouched — children may have been fetched by uniqueKey before the
35531
+ // parent was loaded. Only an explicit value replaces the collection.
35532
+ if (item[propertyName] !== undefined) {
35533
+ updateChildItemIdArray(item[propertyName]);
35591
35534
  }
35592
35535
 
35593
- const updateChildItemIdArray = (valueArray) => {
35594
- const currentIdArray = childItemIdArraySignal.peek();
35595
- if (!Array.isArray(valueArray)) {
35596
- if (currentIdArray.length === 0) return;
35597
- childItemIdArraySignal.value = [];
35598
- return;
35599
- }
35600
- let i = 0;
35601
- const idArray = [];
35602
- let modified = false;
35603
- while (i < valueArray.length) {
35604
- const value = valueArray[i];
35605
- const currentIdAtIndex = currentIdArray[idArray.length];
35606
- i++;
35607
- if (isProps(value)) {
35608
- const childItem = childStore.upsert(value);
35609
- const childItemId = childItem[childIdKey];
35610
- if (currentIdAtIndex !== childItemId) modified = true;
35611
- idArray.push(childItemId);
35612
- continue;
35613
- }
35614
- if (primitiveCanBeId(value)) {
35615
- const childItemProps = { [childIdKey]: value };
35616
- const childItem = childStore.upsert(childItemProps);
35617
- const childItemId = childItem[childIdKey];
35618
- if (currentIdAtIndex !== childItemId) modified = true;
35619
- idArray.push(childItemId);
35620
- continue;
35621
- }
35622
- }
35623
- if (modified || currentIdArray.length !== idArray.length) {
35624
- childItemIdArraySignal.value = idArray;
35625
- }
35626
- };
35627
-
35628
- updateChildItemIdArray(item[propertyName]);
35629
-
35630
35536
  // When an id is renamed (PUT/PATCH changes the idKey), patch the id array.
35631
- syncIdArrayOnRename(childStore, childIdKey, childItemIdArraySignal);
35537
+ syncIdArrayOnRename(childStore, childIdKey, idArraySignal);
35632
35538
 
35633
35539
  const childItemArraySignal = computed(() => {
35634
- const childItemIdArray = childItemIdArraySignal.value;
35635
- const childItemArray = childStore.selectAll(childItemIdArray);
35540
+ const childItemArray = childStore.selectAll(idArraySignal.value);
35636
35541
  Object.defineProperty(childItemArray, SYMBOL_OBJECT_SIGNAL, {
35637
35542
  value: childItemArraySignal,
35638
35543
  writable: false,
@@ -35650,13 +35555,10 @@ ${originalActionName} source location: ${locationInfo}`,
35650
35555
  const createRestActionForScopedMany = (
35651
35556
  verb,
35652
35557
  callback,
35653
- { isMany, lifecycleCtx },
35558
+ { isMany, onActionComplete },
35654
35559
  ) => {
35655
- if (!callback) {
35656
- return undefined;
35657
- }
35658
35560
  const childActionName = `${childName}.${verb}`;
35659
- const childAction = createAction(callback, {
35561
+ return createAction(callback, {
35660
35562
  name: childActionName,
35661
35563
  meta: { verb, isMany, paramScope },
35662
35564
  resultToValue: (result) => {
@@ -35673,48 +35575,33 @@ ${originalActionName} source location: ${locationInfo}`,
35673
35575
  uniqueKeys,
35674
35576
  childActionName,
35675
35577
  );
35676
- let childStore = scopedStoreMap.get(ownerId);
35677
- if (!childStore) {
35678
- // Owner not yet in store — lazily create scoped store so actions can run
35679
- // before the parent item has been fully loaded (e.g. rows fetched before table).
35680
- childStore = arraySignalStore([], childIdKey, {
35681
- name: `${childName}#${ownerId} store`,
35682
- createItem: (props) => {
35683
- const childItem = {};
35684
- Object.assign(childItem, props);
35685
- for (const childSetup of childSetupCallbackSet) {
35686
- childSetup(childItem);
35687
- }
35688
- return childItem;
35689
- },
35690
- });
35691
- scopedStoreMap.set(ownerId, childStore);
35692
- const newIdArraySignal = signal([]);
35693
- scopedIdArraySignalMap.set(ownerId, newIdArraySignal);
35694
- }
35695
- const childItemIdArraySignal = scopedIdArraySignalMap.get(ownerId);
35578
+ // Owner not in store yet: create the scope so actions can run before
35579
+ // the parent item has been loaded (e.g. rows fetched before their table).
35580
+ const scope = scopeMap.get(ownerId) || createScope(ownerId);
35581
+ const { childStore, idArraySignal } = scope;
35696
35582
 
35697
35583
  if (verb === "DELETE") {
35698
35584
  if (isMany) {
35699
35585
  const idArray = childStore.drop(rest[0]);
35700
35586
  const toRemoveSet = new Set(idArray);
35701
- childItemIdArraySignal.value = childItemIdArraySignal
35587
+ idArraySignal.value = idArraySignal
35702
35588
  .peek()
35703
35589
  .filter((id) => !toRemoveSet.has(id));
35704
35590
  return [ownerId, idArray];
35705
35591
  }
35706
35592
  const childId = childStore.drop(rest[0]);
35707
- childItemIdArraySignal.value = childItemIdArraySignal
35593
+ idArraySignal.value = idArraySignal
35708
35594
  .peek()
35709
35595
  .filter((id) => id !== childId);
35710
35596
  return [ownerId, childId];
35711
35597
  }
35712
35598
 
35713
35599
  if (isMany) {
35714
- // GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items
35600
+ // GET_MANY, POST_MANY, PUT_MANY etc: rest[0] is the array of items,
35601
+ // and it replaces the whole collection.
35715
35602
  const itemArray = childStore.upsert(rest[0]);
35716
- const idArray = itemArray.map((i) => i[childIdKey]);
35717
- childItemIdArraySignal.value = idArray;
35603
+ const idArray = itemArray.map((childItem) => childItem[childIdKey]);
35604
+ idArraySignal.value = idArray;
35718
35605
  return [ownerId, idArray];
35719
35606
  }
35720
35607
 
@@ -35726,25 +35613,23 @@ ${originalActionName} source location: ${locationInfo}`,
35726
35613
  return [ownerId, childItem[childIdKey]];
35727
35614
  },
35728
35615
  valueToData: (value) => {
35729
- if (!value) return isMany ? [] : undefined;
35616
+ if (!value) {
35617
+ return isMany ? [] : undefined;
35618
+ }
35730
35619
  const [ownerId, idOrIdArray] = value;
35731
- const childStore = scopedStoreMap.get(ownerId);
35732
- if (!childStore) return isMany ? [] : undefined;
35733
- if (isMany) return childStore.selectAll(idOrIdArray);
35734
- return childStore.select(idOrIdArray);
35735
- },
35736
- completeSideEffect: (actionCompleted) => {
35737
- lifecycleCtx.onComplete(actionCompleted);
35620
+ const scope = scopeMap.get(ownerId);
35621
+ if (!scope) {
35622
+ return isMany ? [] : undefined;
35623
+ }
35624
+ if (isMany) {
35625
+ return scope.childStore.selectAll(idOrIdArray);
35626
+ }
35627
+ return scope.childStore.select(idOrIdArray);
35738
35628
  },
35629
+ completeSideEffect: onActionComplete,
35739
35630
  });
35740
- return childAction;
35741
35631
  };
35742
35632
 
35743
- // When a child (scopedMany) item is mutated via POST, the parent GET must
35744
- // re-fetch because the parent embeds the child array and we cannot know the
35745
- // new ordering without asking the backend again.
35746
- // (scopedOne does NOT need this: the mutation result contains the updated
35747
- // item directly, so no parent re-fetch is necessary.)
35748
35633
  const childResource = createResource(childName, {
35749
35634
  idKey: childIdKey,
35750
35635
  restCallbacks: {
@@ -35766,19 +35651,23 @@ ${originalActionName} source location: ${locationInfo}`,
35766
35651
  rerunOn: scopedManyRerunOn ?? rerunOn,
35767
35652
  dependencies: scopedManyDependencies ?? dependencies,
35768
35653
  });
35769
- // Register: when childResource fires, rerun parent (stateFacade) GETs.
35654
+ // When a scoped child collection is mutated (POST etc.), the parent GET must
35655
+ // re-fetch: the parent embeds the child array and only the backend knows the
35656
+ // new ordering. (scopedOne does not need this: the mutation result contains
35657
+ // the updated object directly.)
35770
35658
  resourceLifecycleManager.addDependency(
35771
35659
  childResource,
35772
35660
  stateFacade,
35773
35661
  propertyName,
35774
35662
  );
35775
- childResource.getChildStore = (ownerKey) => scopedStoreMap.get(ownerKey);
35663
+ childResource.getChildStore = (ownerKey) =>
35664
+ scopeMap.get(ownerKey)?.childStore;
35776
35665
  return childResource;
35777
35666
  };
35778
35667
 
35779
- // expose rest actions on the stateFacade
35668
+ // expose one action (or range reader) per provided rest callback
35780
35669
  for (const [restCallbackKey, restCallback] of Object.entries(restCallbacks)) {
35781
- if (restCallback === undefined) {
35670
+ if (!restCallback) {
35782
35671
  continue;
35783
35672
  }
35784
35673
  if (restCallbackKey === "GET_RANGE") {
@@ -35800,25 +35689,16 @@ ${originalActionName} source location: ${locationInfo}`,
35800
35689
  const verb = isMany
35801
35690
  ? restCallbackKey.replace("_MANY", "")
35802
35691
  : restCallbackKey;
35803
- const restAction = createRestAction(verb, restCallback, {
35692
+ let restAction = createRestAction(verb, restCallback, {
35804
35693
  isMany,
35805
- lifecycleCtx,
35694
+ onActionComplete,
35806
35695
  paramScope,
35807
35696
  });
35808
- if (!restAction) {
35809
- console.error("no action returned (here to see when it happens)");
35810
- continue;
35811
- }
35812
- let actionToRegister;
35813
35697
  if (params) {
35814
- const restActionBound = restAction.bindParams(params);
35815
- stateFacade[restCallbackKey] = restActionBound;
35816
- actionToRegister = restActionBound;
35817
- } else {
35818
- stateFacade[restCallbackKey] = restAction;
35819
- actionToRegister = restAction;
35698
+ restAction = restAction.bindParams(params);
35820
35699
  }
35821
- resourceLifecycleManager.registerAction(stateFacade, actionToRegister);
35700
+ stateFacade[restCallbackKey] = restAction;
35701
+ resourceLifecycleManager.registerAction(stateFacade, restAction);
35822
35702
  }
35823
35703
 
35824
35704
  return stateFacade;
@@ -35829,76 +35709,64 @@ const createRestActionFactoryForRoot = (
35829
35709
  {
35830
35710
  idKey,
35831
35711
  store, // see array_signal_store.js
35712
+ declarationSite,
35832
35713
  },
35833
35714
  ) => {
35834
35715
  const createActionForRoot = (
35835
35716
  verb,
35836
35717
  restCallback,
35837
- { isMany, lifecycleCtx, paramScope },
35718
+ { isMany, onActionComplete, paramScope },
35838
35719
  ) => {
35839
35720
  if (!isMany) {
35840
35721
  return createActionAffectingOneItem(verb, restCallback, {
35841
- lifecycleCtx,
35722
+ onActionComplete,
35842
35723
  paramScope,
35843
35724
  });
35844
35725
  }
35845
35726
  return createActionAffectingManyItems(verb, restCallback, {
35846
- lifecycleCtx,
35727
+ onActionComplete,
35847
35728
  paramScope,
35848
35729
  });
35849
35730
  };
35850
35731
  const createActionAffectingOneItem = (
35851
35732
  verb,
35852
35733
  callback,
35853
- { lifecycleCtx, paramScope },
35734
+ { onActionComplete, paramScope },
35854
35735
  ) => {
35855
35736
  const applyResultToValue =
35856
35737
  verb === "DELETE"
35857
- ? (itemIdOrItemProps) => {
35858
- const itemId = store.drop(itemIdOrItemProps);
35859
- return itemId;
35860
- }
35738
+ ? (itemIdOrItemProps) => store.drop(itemIdOrItemProps)
35861
35739
  : (result) => {
35862
- let item;
35863
- if (Array.isArray(result)) {
35864
- // the callback is returning something like [property, value, props]
35865
- // this is to support a case like:
35866
- // store.upsert("name", "currentName", { name: "newName" })
35867
- // where we want to update the idKey of an item
35868
- item = store.upsert(...result);
35869
- } else {
35870
- item = store.upsert(result);
35871
- }
35872
- const itemId = item[idKey];
35873
- return itemId;
35740
+ // An array result is [property, value, props] — used to rename the
35741
+ // idKey of an item: store.upsert("name", "currentName", { name: "newName" })
35742
+ const item = Array.isArray(result)
35743
+ ? store.upsert(...result)
35744
+ : store.upsert(result);
35745
+ return item[idKey];
35874
35746
  };
35875
-
35876
- const callerInfo = getCallerInfo(null, 2);
35877
- // Provide more fallback options for better debugging
35878
- const locationInfo =
35879
- callerInfo.file && callerInfo.line && callerInfo.column
35880
- ? `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`
35881
- : callerInfo.raw || "unknown location";
35882
- const originalActionName = `${name}.${verb}`;
35883
- const actionAffectingOneItem = createAction(callback, {
35747
+ const throwInvalidResult = createInvalidResultThrower(
35748
+ `${name}.${verb}`,
35749
+ declarationSite,
35750
+ );
35751
+ return createAction(callback, {
35884
35752
  name: `${name}.${verb}`,
35885
35753
  meta: { verb, isMany: false, paramScope },
35886
35754
  resultToValue: (result, action) => {
35887
- const actionLabel = action.name;
35888
-
35889
35755
  if (verb === "DELETE") {
35890
35756
  if (!isProps(result) && !primitiveCanBeId(result)) {
35891
- throw new TypeError(
35892
- `${actionLabel} must return an object (that will be used to drop "${name}" resource), received ${result}.
35893
- ${originalActionName} source location: ${locationInfo}`,
35757
+ throwInvalidResult(
35758
+ action.name,
35759
+ `an object (that will be used to drop "${name}" resource)`,
35760
+ result,
35894
35761
  );
35895
35762
  }
35896
35763
  return applyResultToValue(result);
35897
35764
  }
35898
35765
  if (!isProps(result)) {
35899
- throw new TypeError(
35900
- `${actionLabel} must return an object (that will be used to upsert "${name}" resource), received ${result}.
35901
- ${originalActionName} source location: ${locationInfo}`,
35766
+ throwInvalidResult(
35767
+ action.name,
35768
+ `an object (that will be used to upsert "${name}" resource)`,
35769
+ result,
35902
35770
  );
35903
35771
  }
35904
35772
  // Track which top-level properties the GET response contained so that
@@ -35909,40 +35777,30 @@ ${originalActionName} source location: ${locationInfo}`,
35909
35777
  return applyResultToValue(result);
35910
35778
  },
35911
35779
  valueToData: (itemId) => store.select(itemId),
35912
- completeSideEffect: (actionCompleted) => {
35913
- lifecycleCtx.onComplete(actionCompleted);
35914
- },
35780
+ completeSideEffect: onActionComplete,
35915
35781
  });
35916
- return actionAffectingOneItem;
35917
35782
  };
35918
35783
  const createActionAffectingManyItems = (
35919
35784
  verb,
35920
35785
  callback,
35921
- { lifecycleCtx, paramScope },
35786
+ { onActionComplete, paramScope },
35922
35787
  ) => {
35923
35788
  const applyResultToValue =
35924
35789
  verb === "DELETE"
35925
- ? (idOrMutableIdArray) => {
35926
- const idArray = store.drop(idOrMutableIdArray);
35927
- return idArray;
35928
- }
35790
+ ? (idOrMutableIdArray) => store.drop(idOrMutableIdArray)
35929
35791
  : (dataArray) => {
35930
35792
  const itemArray = store.upsert(dataArray);
35931
- const idArray = itemArray.map((item) => item[idKey]);
35932
- return idArray;
35793
+ return itemArray.map((item) => item[idKey]);
35933
35794
  };
35934
35795
 
35935
- const actionAffectingManyItems = createAction(callback, {
35796
+ return createAction(callback, {
35936
35797
  meta: { verb, isMany: true, paramScope },
35937
35798
  name: `${name}.${verb}_MANY`,
35938
35799
  dataDefault: [],
35939
35800
  resultToValue: applyResultToValue,
35940
- valueToData: (idArray) => {
35941
- const items = store.selectAll(idArray);
35942
- return items;
35943
- },
35801
+ valueToData: (idArray) => store.selectAll(idArray),
35944
35802
  completeSideEffect: (actionCompleted) => {
35945
- lifecycleCtx.onComplete(actionCompleted);
35803
+ onActionComplete(actionCompleted);
35946
35804
  if (
35947
35805
  verb === "DELETE" ||
35948
35806
  actionCompleted.valueSignal.peek().length === 0
@@ -35956,12 +35814,70 @@ ${originalActionName} source location: ${locationInfo}`,
35956
35814
  return syncIdArrayOnRename(store, idKey, actionCompleted.valueSignal);
35957
35815
  },
35958
35816
  });
35959
- return actionAffectingManyItems;
35960
35817
  };
35961
35818
 
35962
35819
  return createActionForRoot;
35963
35820
  };
35964
35821
 
35822
+ // Captures the "file:line:column" of the user code that invoked the public
35823
+ // function (resource(), .one(), .many(), withParams, …), so invalid-result
35824
+ // errors can point at where the callbacks were declared, not at this file.
35825
+ // Must be called directly from the public function: the stack offset accounts
35826
+ // for exactly two frames (getCallerInfo → getDeclarationSite → public fn → user code).
35827
+ const getDeclarationSite = () => {
35828
+ const callerInfo = getCallerInfo(null, 1);
35829
+ if (callerInfo.file && callerInfo.line && callerInfo.column) {
35830
+ return `${callerInfo.file}:${callerInfo.line}:${callerInfo.column}`;
35831
+ }
35832
+ return callerInfo.raw || "unknown location";
35833
+ };
35834
+
35835
+ const createInvalidResultThrower = (originalActionName, declarationSite) => {
35836
+ return (actionLabel, expected, result) => {
35837
+ throw new TypeError(
35838
+ `${actionLabel} must return ${expected}, received ${result}.
35839
+ ${originalActionName} source location: ${declarationSite}`,
35840
+ );
35841
+ };
35842
+ };
35843
+
35844
+ // Shared by .many() and .scopedMany(): converts a raw relationship value (an
35845
+ // array of child props/ids, or anything else meaning "empty") into an array of
35846
+ // child ids, upserting each entry into the child store. The id array signal is
35847
+ // only touched when the resulting ids actually differ.
35848
+ const createChildIdArrayUpdater = (childStore, childIdKey, idArraySignal) => {
35849
+ return (valueArray) => {
35850
+ const currentIdArray = idArraySignal.peek();
35851
+ if (!Array.isArray(valueArray)) {
35852
+ if (currentIdArray.length > 0) {
35853
+ idArraySignal.value = [];
35854
+ }
35855
+ return;
35856
+ }
35857
+ const idArray = [];
35858
+ let modified = false;
35859
+ for (const value of valueArray) {
35860
+ let childItemProps;
35861
+ if (isProps(value)) {
35862
+ childItemProps = value;
35863
+ } else if (primitiveCanBeId(value)) {
35864
+ childItemProps = { [childIdKey]: value };
35865
+ } else {
35866
+ continue;
35867
+ }
35868
+ const childItem = childStore.upsert(childItemProps);
35869
+ const childItemId = childItem[childIdKey];
35870
+ if (currentIdArray[idArray.length] !== childItemId) {
35871
+ modified = true;
35872
+ }
35873
+ idArray.push(childItemId);
35874
+ }
35875
+ if (modified || currentIdArray.length !== idArray.length) {
35876
+ idArraySignal.value = idArray;
35877
+ }
35878
+ };
35879
+ };
35880
+
35965
35881
  const syncIdArrayOnRename = (store, idKey, idArraySignal) => {
35966
35882
  return store.observeProperties((mutations) => {
35967
35883
  const idArray = idArraySignal.peek();
@@ -36021,7 +35937,7 @@ const resolveOwnerId = (rawOwnerId, store, idKey, uniqueKeys, actionName) => {
36021
35937
  return item[idKey];
36022
35938
  }
36023
35939
  throw new TypeError(
36024
- `${actionName}: the first element of the returned array is { ${propName}: "${propValue}" } but "${propName}" is neither the idKey ("${idKey}") nor a declared uniqueKey (${uniqueKeys.length ? uniqueKeys.join(", ") : "none"}).
35940
+ `${actionName}: the first element of the returned array is { ${propName}: "${propValue}" } but "${propName}" is neither the idKey ("${idKey}") nor a declared uniqueKey (${uniqueKeys.length ? uniqueKeys.join(", ") : "none"}).
36025
35941
  Return a primitive id or a single-property object whose key is the idKey or a uniqueKey.`,
36026
35942
  );
36027
35943
  }
@@ -36030,7 +35946,7 @@ Return a primitive id or a single-property object whose key is the idKey or a un
36030
35946
  if (idKey in rawOwnerId) {
36031
35947
  const resolvedId = rawOwnerId[idKey];
36032
35948
  console.warn(
36033
- `${actionName}: the first element of the returned array is an object with multiple properties.
35949
+ `${actionName}: the first element of the returned array is an object with multiple properties.
36034
35950
  Only "${idKey}" is needed. Consider returning a primitive id or { ${idKey}: value } instead.`,
36035
35951
  );
36036
35952
  return resolvedId;
@@ -36042,8 +35958,10 @@ Received an object with keys: ${keys.join(", ")}.`,
36042
35958
  );
36043
35959
  };
36044
35960
 
36045
- /** so that when a tracked property changes
36046
- * on an item the corresponding signal is updated automatically.
35961
+ /**
35962
+ * Keeps external signals in sync with properties of the resource's store items:
35963
+ * when a tracked property changes on an item, the corresponding signal is
35964
+ * updated automatically.
36047
35965
  *
36048
35966
  * Since signals are typically connected to route parameters via the route template
36049
35967
  * syntax, this keeps the URL in sync when a store item's mutable key is renamed.
@@ -38598,7 +38516,7 @@ const ROUTE_TRAVEL_ATTRIBUTE = "data-navi-route-travel";
38598
38516
  // the root pictures must NOT move (they carry the whole viewport, blank bands
38599
38517
  // included).
38600
38518
 
38601
- const css$V = /* css */`
38519
+ const css$W = /* css */`
38602
38520
  /* The marked region is a picture of its own for the length of a transition of
38603
38521
  OURS, and only then — the name is what makes the pages a picture the
38604
38522
  movement below can carry.
@@ -39027,7 +38945,7 @@ const RouteTransitionArea = ({
39027
38945
  children,
39028
38946
  ...rest
39029
38947
  }) => {
39030
- import.meta.css = [css$V, "@jsenv/navi/src/nav/route_transition.jsx"];
38948
+ import.meta.css = [css$W, "@jsenv/navi/src/nav/route_transition.jsx"];
39031
38949
  const props = {
39032
38950
  ...rest,
39033
38951
  [TRANSITION_AREA_ATTRIBUTE]: ""
@@ -39084,7 +39002,7 @@ const RouteTransitionArea = ({
39084
39002
  * @returns {() => void} remove this relation.
39085
39003
  */
39086
39004
  const defineRouteTransition = (from, to, transition) => {
39087
- import.meta.css = [css$V, "@jsenv/navi/src/nav/route_transition.jsx"];
39005
+ import.meta.css = [css$W, "@jsenv/navi/src/nav/route_transition.jsx"];
39088
39006
  const {
39089
39007
  type,
39090
39008
  duration
@@ -39120,7 +39038,7 @@ const defineRouteTransition = (from, to, transition) => {
39120
39038
  * @returns {() => void} remove this default.
39121
39039
  */
39122
39040
  const defineRouteDefaultTransition = transition => {
39123
- import.meta.css = [css$V, "@jsenv/navi/src/nav/route_transition.jsx"];
39041
+ import.meta.css = [css$W, "@jsenv/navi/src/nav/route_transition.jsx"];
39124
39042
  const value = normalizeTransition(transition);
39125
39043
  defaultTransition = value;
39126
39044
  return () => {
@@ -39721,7 +39639,7 @@ const DRAGGED_ATTRIBUTE = "data-navi-route-travel-dragged";
39721
39639
  const TURNED_ATTRIBUTE = "data-navi-route-travel-turned";
39722
39640
  // The name the box wears while it travels, and only then (see nameForTravel).
39723
39641
  const TRAVEL_NAME = "navi-route-travel";
39724
- const css$U = /* css */`
39642
+ const css$V = /* css */`
39725
39643
  /* The name that makes the page inside this box a picture of its own during a
39726
39644
  transition — rather than part of the one big picture the document takes, so
39727
39645
  the two pages can move past each other while everything else stays where it
@@ -40111,7 +40029,7 @@ const RouteTravel = ({
40111
40029
  children,
40112
40030
  ...rest
40113
40031
  }) => {
40114
- import.meta.css = [css$U, "@jsenv/navi/src/nav/route_travel.jsx"];
40032
+ import.meta.css = [css$V, "@jsenv/navi/src/nav/route_travel.jsx"];
40115
40033
  const elementRef = useRef();
40116
40034
  const gestureRef = useRef(null);
40117
40035
  // The travel in hand: the transition keeping the picture of the page being
@@ -42673,7 +42591,7 @@ const PhoneSvg = () => {
42673
42591
  };
42674
42592
 
42675
42593
  installImportMetaCssBuild(import.meta);// # TextAnchor — how it works
42676
- const css$T = /* css */`
42594
+ const css$U = /* css */`
42677
42595
  .navi_text_anchor {
42678
42596
  vertical-align: baseline;
42679
42597
  user-select: none;
@@ -42708,7 +42626,7 @@ const TextAnchor = ({
42708
42626
  textSize,
42709
42627
  lineLayout
42710
42628
  }) => {
42711
- import.meta.css = [css$T, "@jsenv/navi/src/text/text_anchor.jsx"];
42629
+ import.meta.css = [css$U, "@jsenv/navi/src/text/text_anchor.jsx"];
42712
42630
  const anchorRef = useRef();
42713
42631
 
42714
42632
  // Plain useLayoutEffect would also fire while an ancestor dialog/popover
@@ -42823,7 +42741,7 @@ const computeTopOffset = ({
42823
42741
  };
42824
42742
  const charTopCanvas = document.createElement("canvas");
42825
42743
 
42826
- installImportMetaCssBuild(import.meta);const css$S = /* css */`
42744
+ installImportMetaCssBuild(import.meta);const css$T = /* css */`
42827
42745
  @layer navi {
42828
42746
  /* Ensure data attributes from box.jsx can win to update display */
42829
42747
  .navi_icon {
@@ -42981,7 +42899,7 @@ const Icon = ({
42981
42899
  fillLine,
42982
42900
  ...props
42983
42901
  }) => {
42984
- import.meta.css = [css$S, "@jsenv/navi/src/text/icon.jsx"];
42902
+ import.meta.css = [css$T, "@jsenv/navi/src/text/icon.jsx"];
42985
42903
  const innerChildren = href ? jsx("svg", {
42986
42904
  width: "100%",
42987
42905
  height: "100%",
@@ -43143,7 +43061,7 @@ const useDimColorWhen = (elementRef, shouldDim) => {
43143
43061
  });
43144
43062
  };
43145
43063
 
43146
- installImportMetaCssBuild(import.meta);const css$R = /* css */`
43064
+ installImportMetaCssBuild(import.meta);const css$S = /* css */`
43147
43065
  @layer navi {
43148
43066
  .navi_link {
43149
43067
  --link-border-radius: unset;
@@ -43605,7 +43523,7 @@ Object.assign(PSEUDO_CLASSES, {
43605
43523
  * @param {boolean} [props.readOnly]
43606
43524
  */
43607
43525
  const Link = props => {
43608
- import.meta.css = [css$R, "@jsenv/navi/src/nav/link/link.jsx"];
43526
+ import.meta.css = [css$S, "@jsenv/navi/src/nav/link/link.jsx"];
43609
43527
  if (props.route) {
43610
43528
  return jsx(LinkWithRoute, {
43611
43529
  ...props
@@ -43928,7 +43846,7 @@ installImportMetaCssBuild(import.meta);/**
43928
43846
  * https://dribbble.com/search/tabs
43929
43847
  */
43930
43848
  let navCount = 0;
43931
- const css$Q = /* css */`
43849
+ const css$R = /* css */`
43932
43850
  @layer navi {
43933
43851
  .navi_nav {
43934
43852
  --nav-border: none;
@@ -44211,7 +44129,7 @@ const Nav = ({
44211
44129
  slideContainer,
44212
44130
  ...props
44213
44131
  }) => {
44214
- import.meta.css = [css$Q, "@jsenv/navi/src/nav/link/nav.jsx"];
44132
+ import.meta.css = [css$R, "@jsenv/navi/src/nav/link/nav.jsx"];
44215
44133
  const defaultRef = useRef();
44216
44134
  props.ref = props.ref || defaultRef;
44217
44135
  const navRef = props.ref;
@@ -44738,7 +44656,7 @@ installImportMetaCssBuild(import.meta);/**
44738
44656
  * Border width participates in layout (it is added to the tab and page
44739
44657
  * padding): a thick border grows the binder rather than eating into the text.
44740
44658
  */
44741
- const css$P = /* css */`
44659
+ const css$Q = /* css */`
44742
44660
  @layer navi {
44743
44661
  .navi_binder {
44744
44662
  --binder-border-width: var(--navi-control-border-width);
@@ -45051,7 +44969,7 @@ const Binder = ({
45051
44969
  pagePadding,
45052
44970
  ...props
45053
44971
  }) => {
45054
- import.meta.css = [css$P, "@jsenv/navi/src/nav/binder/binder.jsx"];
44972
+ import.meta.css = [css$Q, "@jsenv/navi/src/nav/binder/binder.jsx"];
45055
44973
  const items = toChildArray(children).map((child, index) => {
45056
44974
  const {
45057
44975
  value: itemValue,
@@ -45538,7 +45456,7 @@ installImportMetaCssBuild(import.meta);/**
45538
45456
  * added to the size asked for exactly like the notch inset is, so the
45539
45457
  * content still gets the size the prop names.
45540
45458
  */
45541
- const css$O = /* css */`
45459
+ const css$P = /* css */`
45542
45460
  @layer navi {
45543
45461
  :root {
45544
45462
  --navi-fixed-bar-width: 56px;
@@ -45686,7 +45604,7 @@ const FixedBar = ({
45686
45604
  border = true,
45687
45605
  ...props
45688
45606
  }) => {
45689
- import.meta.css = [css$O, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
45607
+ import.meta.css = [css$P, "@jsenv/navi/src/layout/fixed_bar/fixed_bar.jsx"];
45690
45608
  const defaultRef = useRef();
45691
45609
  props.ref = props.ref || defaultRef;
45692
45610
  // Said with the width the border rule reads rather than with an attribute of
@@ -45780,7 +45698,7 @@ const FixedBar = ({
45780
45698
  // Subpixel layout rounds rectangles up on boxes that fit exactly.
45781
45699
  const OVERFLOW_TOLERANCE = 1;
45782
45700
 
45783
- const css$N = /* css */ `
45701
+ const css$O = /* css */ `
45784
45702
  [data-navi-overflow-x] {
45785
45703
  outline: 2px dashed #e74c3c;
45786
45704
  outline-offset: -2px;
@@ -45804,7 +45722,7 @@ const detectHorizontalOverflow = ({
45804
45722
  let styleEl = null;
45805
45723
  if (highlight) {
45806
45724
  styleEl = document.createElement("style");
45807
- styleEl.textContent = css$N;
45725
+ styleEl.textContent = css$O;
45808
45726
  document.head.appendChild(styleEl);
45809
45727
  }
45810
45728
 
@@ -45959,7 +45877,7 @@ const useFocusGroup = (
45959
45877
  };
45960
45878
 
45961
45879
  installImportMetaCssBuild(import.meta);const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z";
45962
- const css$M = /* css */`
45880
+ const css$N = /* css */`
45963
45881
  .navi_summary_marker {
45964
45882
  width: 1em;
45965
45883
  height: 1em;
@@ -46049,7 +45967,7 @@ const SummaryMarker = ({
46049
45967
  loading,
46050
45968
  openDirection = "down"
46051
45969
  }) => {
46052
- import.meta.css = [css$M, "@jsenv/navi/src/control/details/summary_marker.jsx"];
45970
+ import.meta.css = [css$N, "@jsenv/navi/src/control/details/summary_marker.jsx"];
46053
45971
  const showLoading = useDebounceTrue(loading, 300);
46054
45972
  return jsx("span", {
46055
45973
  className: "navi_summary_marker",
@@ -46094,7 +46012,7 @@ const SummaryMarker = ({
46094
46012
  });
46095
46013
  };
46096
46014
 
46097
- installImportMetaCssBuild(import.meta);const css$L = /* css */`
46015
+ installImportMetaCssBuild(import.meta);const css$M = /* css */`
46098
46016
  .navi_details {
46099
46017
  position: relative;
46100
46018
  z-index: 1;
@@ -46140,7 +46058,7 @@ const Details = props => {
46140
46058
  return details;
46141
46059
  };
46142
46060
  const DetailsField = props => {
46143
- import.meta.css = [css$L, "@jsenv/navi/src/control/details/details.jsx"];
46061
+ import.meta.css = [css$M, "@jsenv/navi/src/control/details/details.jsx"];
46144
46062
  const {
46145
46063
  ref,
46146
46064
  persists,
@@ -46360,7 +46278,7 @@ installImportMetaCssBuild(import.meta);/**
46360
46278
  * UI when it comes first). Once settled open the clipping is released, so a
46361
46279
  * popover or focus ring inside is not cut at the edges.
46362
46280
  */
46363
- const css$K = /* css */`
46281
+ const css$L = /* css */`
46364
46282
  .navi_expandable {
46365
46283
  position: relative;
46366
46284
  display: flex;
@@ -46586,7 +46504,7 @@ const useExpandableContext = partName => {
46586
46504
  * and rebuilds it from scratch on every expansion.
46587
46505
  */
46588
46506
  const Expandable = props => {
46589
- import.meta.css = [css$K, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46507
+ import.meta.css = [css$L, "@jsenv/navi/src/control/expandable/expandable.jsx"];
46590
46508
  const {
46591
46509
  ref,
46592
46510
  ui,
@@ -47251,7 +47169,7 @@ const ControlGroup = props => {
47251
47169
  };
47252
47170
  const CONTROL_GROUP_PSEUDO_CLASSES = [":hover", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
47253
47171
 
47254
- installImportMetaCssBuild(import.meta);const css$J = /* css */`
47172
+ installImportMetaCssBuild(import.meta);const css$K = /* css */`
47255
47173
  @layer navi {
47256
47174
  .navi_checkbox {
47257
47175
  --switch-margin: 0; /* Useful to reserve space for outline */
@@ -47329,7 +47247,7 @@ installImportMetaCssBuild(import.meta);const css$J = /* css */`
47329
47247
  }
47330
47248
  `;
47331
47249
  const SwitchUI = () => {
47332
- import.meta.css = [css$J, "@jsenv/navi/src/control/input/switch_ui.jsx"];
47250
+ import.meta.css = [css$K, "@jsenv/navi/src/control/input/switch_ui.jsx"];
47333
47251
  return jsx(Box, {
47334
47252
  className: "navi_switch",
47335
47253
  as: "svg",
@@ -47371,7 +47289,7 @@ const useCheckableProps = (props, options) => {
47371
47289
  return result;
47372
47290
  };
47373
47291
 
47374
- installImportMetaCssBuild(import.meta);const css$I = /* css */`
47292
+ installImportMetaCssBuild(import.meta);const css$J = /* css */`
47375
47293
  @layer navi {
47376
47294
  .navi_checkbox {
47377
47295
  --border-radius: var(--navi-checkbox-border-radius);
@@ -47698,7 +47616,7 @@ const InputCheckboxHeadless = props => {
47698
47616
  });
47699
47617
  };
47700
47618
  const InputCheckboxFieldInterface = props => {
47701
- import.meta.css = [css$I, "@jsenv/navi/src/control/input/input_checkbox.jsx"];
47619
+ import.meta.css = [css$J, "@jsenv/navi/src/control/input/input_checkbox.jsx"];
47702
47620
  const [checkboxRootProps, checkboxHostProps] = useCheckableProps(props);
47703
47621
  const {
47704
47622
  icon,
@@ -47820,7 +47738,7 @@ const CheckboxButtonStyleCSSVars = {
47820
47738
  const CheckboxPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
47821
47739
  const CheckboxPseudoElements = ["::-navi-loader", "::-navi-checkmark"];
47822
47740
 
47823
- installImportMetaCssBuild(import.meta);const css$H = /* css */`
47741
+ installImportMetaCssBuild(import.meta);const css$I = /* css */`
47824
47742
  @layer navi {
47825
47743
  .navi_label {
47826
47744
  --label-required-indicator-color: var(--navi-color-danger, #b42318);
@@ -47900,7 +47818,7 @@ installImportMetaCssBuild(import.meta);const css$H = /* css */`
47900
47818
  * </Field>
47901
47819
  */
47902
47820
  const Field = props => {
47903
- import.meta.css = [css$H, "@jsenv/navi/src/control/field.jsx"];
47821
+ import.meta.css = [css$I, "@jsenv/navi/src/control/field.jsx"];
47904
47822
  const refDefault = useRef();
47905
47823
  props.ref = props.ref || refDefault;
47906
47824
  const {
@@ -47935,7 +47853,7 @@ const FieldCSSVars = {
47935
47853
  spacingWithControl: "--spacing-with-control"
47936
47854
  };
47937
47855
  const FieldAsContainer = props => {
47938
- import.meta.css = [css$H, "@jsenv/navi/src/control/field.jsx"];
47856
+ import.meta.css = [css$I, "@jsenv/navi/src/control/field.jsx"];
47939
47857
  const {
47940
47858
  children
47941
47859
  } = props;
@@ -47967,7 +47885,7 @@ const FieldAsContainer = props => {
47967
47885
  };
47968
47886
  const FIELD_PSEUDO_CLASSES = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":-navi-loading"];
47969
47887
  const Label = props => {
47970
- import.meta.css = [css$H, "@jsenv/navi/src/control/field.jsx"];
47888
+ import.meta.css = [css$I, "@jsenv/navi/src/control/field.jsx"];
47971
47889
  const {
47972
47890
  children,
47973
47891
  // Marks the label when its control is required. Takes what to show, or
@@ -48129,7 +48047,7 @@ const InputSlot = ({
48129
48047
  });
48130
48048
  };
48131
48049
 
48132
- installImportMetaCssBuild(import.meta);const css$G = /* css */`
48050
+ installImportMetaCssBuild(import.meta);const css$H = /* css */`
48133
48051
  @layer navi {
48134
48052
  .navi_radio {
48135
48053
  --margin: 3px 3px 3px 5px;
@@ -48492,7 +48410,7 @@ const InputRadioHeadless = props => {
48492
48410
  };
48493
48411
  const VARIANT_SET = new Set(["icon", "button", "radio"]);
48494
48412
  const InputRadioFieldInterface = props => {
48495
- import.meta.css = [css$G, "@jsenv/navi/src/control/input/input_radio.jsx"];
48413
+ import.meta.css = [css$H, "@jsenv/navi/src/control/input/input_radio.jsx"];
48496
48414
  const [radioRootProps, radioHostProps] = useCheckableProps(props);
48497
48415
  const {
48498
48416
  icon,
@@ -48638,7 +48556,7 @@ const RadioButtonStyleCSSVars = {
48638
48556
  const RadioPseudoClasses = [":hover", ":active", ":focus", ":focus-visible", ":read-only", ":disabled", ":checked", ":-navi-loading"];
48639
48557
  const RadioPseudoElements = ["::-navi-loader", "::-navi-radiomark"];
48640
48558
 
48641
- installImportMetaCssBuild(import.meta);const css$F = /* css */`
48559
+ installImportMetaCssBuild(import.meta);const css$G = /* css */`
48642
48560
  @layer navi {
48643
48561
  .navi_input_range {
48644
48562
  --border-radius: 6px;
@@ -48893,7 +48811,7 @@ const InputRange = props => {
48893
48811
  });
48894
48812
  };
48895
48813
  const InputRangeFieldInterface = props => {
48896
- import.meta.css = [css$F, "@jsenv/navi/src/control/input/input_range.jsx"];
48814
+ import.meta.css = [css$G, "@jsenv/navi/src/control/input/input_range.jsx"];
48897
48815
  const {
48898
48816
  ref
48899
48817
  } = props;
@@ -51001,7 +50919,7 @@ installImportMetaCssBuild(import.meta);/**
51001
50919
  * This means an editable thing MUST have a parent with position relative that wraps the content and the eventual editable input
51002
50920
  *
51003
50921
  */
51004
- const css$E = /* css */`
50922
+ const css$F = /* css */`
51005
50923
  .navi_editable_wrapper {
51006
50924
  --inset-top: 0px;
51007
50925
  --inset-right: 0px;
@@ -51050,7 +50968,7 @@ const useEditionController = () => {
51050
50968
  };
51051
50969
  };
51052
50970
  const Editable = props => {
51053
- import.meta.css = [css$E, "@jsenv/navi/src/control/edition/editable.jsx"];
50971
+ import.meta.css = [css$F, "@jsenv/navi/src/control/edition/editable.jsx"];
51054
50972
  let {
51055
50973
  children,
51056
50974
  action,
@@ -51599,7 +51517,7 @@ installImportMetaCssBuild(import.meta);/**
51599
51517
  * meet are drawn once instead of twice, and only the outer corners stay
51600
51518
  * rounded. See docs/control_group.md.
51601
51519
  */
51602
- const css$D = /* css */`
51520
+ const css$E = /* css */`
51603
51521
  .navi_group {
51604
51522
  --group-border-width: var(--navi-control-border-width);
51605
51523
 
@@ -51715,7 +51633,7 @@ const Group = ({
51715
51633
  vertical = row,
51716
51634
  ...props
51717
51635
  }) => {
51718
- import.meta.css = [css$D, "@jsenv/navi/src/control/group.jsx"];
51636
+ import.meta.css = [css$E, "@jsenv/navi/src/control/group.jsx"];
51719
51637
  return jsx(Box, {
51720
51638
  baseClassName: "navi_group",
51721
51639
  "data-vertical": vertical ? "" : undefined
@@ -51881,7 +51799,7 @@ installImportMetaCssBuild(import.meta);/**
51881
51799
  * So: nothing scrollable between the cap and the slides (a shared [data-body]
51882
51800
  * around them IS a scroller, see box.jsx), and `overflow="auto"` on each Slide.
51883
51801
  */
51884
- const css$C = /* css */`
51802
+ const css$D = /* css */`
51885
51803
  /* Where the picture stands relative to the slide that is current, in boxes
51886
51804
  (see paintTravelProgress). Declared, so that it is a NUMBER the browser can
51887
51805
  interpolate: the trait an indicator draws has to travel with the slides,
@@ -52375,7 +52293,7 @@ const SlideContainer = ({
52375
52293
  children,
52376
52294
  ...rest
52377
52295
  }) => {
52378
- import.meta.css = [css$C, "@jsenv/navi/src/layout/slide_container.jsx"];
52296
+ import.meta.css = [css$D, "@jsenv/navi/src/layout/slide_container.jsx"];
52379
52297
  const debugFocus = useDebugFocus();
52380
52298
  const trackRef = useRef();
52381
52299
  // The box itself: it is what takes the keyboard when what is on screen holds
@@ -55220,7 +55138,7 @@ installImportMetaCssBuild(import.meta);/**
55220
55138
  * and only under its own `sizeFromAnchor`) pass through untouched via
55221
55139
  * `...rest` to whichever of Popover/Dialog actually renders.
55222
55140
  */
55223
- const css$B = /* css */`
55141
+ const css$C = /* css */`
55224
55142
  @layer navi {
55225
55143
  .navi_popup {
55226
55144
  --popup-border-radius: var(--navi-popup-border-radius);
@@ -55337,7 +55255,7 @@ const css$B = /* css */`
55337
55255
  * @param {import("ignore:preact").ComponentChildren} props.children
55338
55256
  */
55339
55257
  const Popup = props => {
55340
- import.meta.css = [css$B, "@jsenv/navi/src/layout/popup.jsx"];
55258
+ import.meta.css = [css$C, "@jsenv/navi/src/layout/popup.jsx"];
55341
55259
  const {
55342
55260
  mode: modeProp,
55343
55261
  maxWidth,
@@ -55398,7 +55316,7 @@ const Popup = props => {
55398
55316
  });
55399
55317
  };
55400
55318
 
55401
- installImportMetaCssBuild(import.meta);const css$A = /* css */`
55319
+ installImportMetaCssBuild(import.meta);const css$B = /* css */`
55402
55320
  .navi_picker {
55403
55321
  /* Sizing ceilings (maxmax), background, box-shadow, outline, padding,
55404
55322
  overflow... are already handled correctly by Popup/Popover/Dialog
@@ -55526,7 +55444,7 @@ installImportMetaCssBuild(import.meta);const css$A = /* css */`
55526
55444
  }
55527
55445
  `;
55528
55446
  const PickerCustomResolver = props => {
55529
- import.meta.css = [css$A, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
55447
+ import.meta.css = [css$B, "@jsenv/navi/src/control/picker/picker_custom.jsx"];
55530
55448
  if (props.children === undefined) {
55531
55449
  return jsx(PickerNative, {
55532
55450
  ...props
@@ -56320,7 +56238,7 @@ const LoadingIndicator = ({
56320
56238
  });
56321
56239
  };
56322
56240
 
56323
- installImportMetaCssBuild(import.meta);const css$z = /* css */`
56241
+ installImportMetaCssBuild(import.meta);const css$A = /* css */`
56324
56242
  @layer navi {
56325
56243
  .navi_separator {
56326
56244
  --size: 1px;
@@ -56398,7 +56316,7 @@ const Separator = ({
56398
56316
  style,
56399
56317
  ...props
56400
56318
  }) => {
56401
- import.meta.css = [css$z, "@jsenv/navi/src/layout/separator.jsx"];
56319
+ import.meta.css = [css$A, "@jsenv/navi/src/layout/separator.jsx"];
56402
56320
  return jsx(Box, {
56403
56321
  as: vertical ? "span" : "hr",
56404
56322
  ...props,
@@ -56891,7 +56809,7 @@ const ListItemFooter = props => {
56891
56809
  });
56892
56810
  };
56893
56811
 
56894
- installImportMetaCssBuild(import.meta);const css$y = /* css */`
56812
+ installImportMetaCssBuild(import.meta);const css$z = /* css */`
56895
56813
  @layer navi {
56896
56814
  .navi_list_container[navi-selectable] {
56897
56815
  /* Focus outline */
@@ -57103,7 +57021,7 @@ const ListSelectableResolver = props => {
57103
57021
  };
57104
57022
  const ListSelectable = props => {
57105
57023
  const Next = useNextResolver();
57106
- import.meta.css = [css$y, "@jsenv/navi/src/control/list/list_selectable.jsx"];
57024
+ import.meta.css = [css$z, "@jsenv/navi/src/control/list/list_selectable.jsx"];
57107
57025
  // we allow ourselves to auto-generate a name
57108
57026
  const defaultName = useId();
57109
57027
  props.name = props.name || `listbox_${defaultName}`;
@@ -57723,7 +57641,7 @@ const ListVirtualContext = createContext(null);
57723
57641
  // that returning a component of one's own — instead of a bare <List.Item> —
57724
57642
  // works the same way.
57725
57643
  const ListRowContext = createContext(null);
57726
- const css$x = /* css */`
57644
+ const css$y = /* css */`
57727
57645
  @layer navi {
57728
57646
  .navi_list_container {
57729
57647
  --list-outline-width: 1px;
@@ -58422,7 +58340,7 @@ const css$x = /* css */`
58422
58340
  }
58423
58341
  `;
58424
58342
  const ListUI = props => {
58425
- import.meta.css = [css$x, "@jsenv/navi/src/control/list/list.jsx"];
58343
+ import.meta.css = [css$y, "@jsenv/navi/src/control/list/list.jsx"];
58426
58344
  const {
58427
58345
  ref,
58428
58346
  renderBudget: renderBudgetProp = RENDER_BUDGET_DEFAULT,
@@ -62091,7 +62009,7 @@ const PickerPresetResolver = props => {
62091
62009
  });
62092
62010
  };
62093
62011
 
62094
- installImportMetaCssBuild(import.meta);const css$w = /* css */`
62012
+ installImportMetaCssBuild(import.meta);const css$x = /* css */`
62095
62013
  @layer navi {
62096
62014
  }
62097
62015
  .navi_badge {
@@ -62203,7 +62121,7 @@ const Badge = ({
62203
62121
  className,
62204
62122
  ...props
62205
62123
  }) => {
62206
- import.meta.css = [css$w, "@jsenv/navi/src/text/badge.jsx"];
62124
+ import.meta.css = [css$x, "@jsenv/navi/src/text/badge.jsx"];
62207
62125
  const defaultRef = useRef();
62208
62126
  props.ref = props.ref || defaultRef;
62209
62127
  const {
@@ -62255,7 +62173,7 @@ const BadgeButton = props => {
62255
62173
  };
62256
62174
  Badge.Button = BadgeButton;
62257
62175
 
62258
- installImportMetaCssBuild(import.meta);const css$v = /* css */`
62176
+ installImportMetaCssBuild(import.meta);const css$w = /* css */`
62259
62177
  @layer navi {
62260
62178
  }
62261
62179
  .navi_badge_list {
@@ -62280,7 +62198,7 @@ const BadgeList = ({
62280
62198
  max,
62281
62199
  ...props
62282
62200
  }) => {
62283
- import.meta.css = [css$v, "@jsenv/navi/src/text/badge_list.jsx"];
62201
+ import.meta.css = [css$w, "@jsenv/navi/src/text/badge_list.jsx"];
62284
62202
  const measureRef = useRef();
62285
62203
  const visibleRef = useRef();
62286
62204
  useLayoutEffect(() => {
@@ -62355,7 +62273,7 @@ const BadgeList = ({
62355
62273
  });
62356
62274
  };
62357
62275
 
62358
- installImportMetaCssBuild(import.meta);const css$u = /* css */`
62276
+ installImportMetaCssBuild(import.meta);const css$v = /* css */`
62359
62277
  .navi_color {
62360
62278
  display: block;
62361
62279
  aspect-ratio: 1/1;
@@ -62386,7 +62304,7 @@ const Color = ({
62386
62304
  children,
62387
62305
  ...rest
62388
62306
  }) => {
62389
- import.meta.css = [css$u, "@jsenv/navi/src/text/color.jsx"];
62307
+ import.meta.css = [css$v, "@jsenv/navi/src/text/color.jsx"];
62390
62308
  const color = children || undefined;
62391
62309
  return jsx(Box, {
62392
62310
  as: "span",
@@ -62843,7 +62761,7 @@ const PickerFileUI = () => {
62843
62761
  return String(value);
62844
62762
  };
62845
62763
 
62846
- installImportMetaCssBuild(import.meta);const css$t = /* css */`
62764
+ installImportMetaCssBuild(import.meta);const css$u = /* css */`
62847
62765
  @layer navi {
62848
62766
  .navi_picker {
62849
62767
  --picker-border-radius: var(--navi-control-border-radius);
@@ -63232,7 +63150,7 @@ installImportMetaCssBuild(import.meta);const css$t = /* css */`
63232
63150
  }
63233
63151
  `;
63234
63152
  const PickerButton = props => {
63235
- import.meta.css = [css$t, "@jsenv/navi/src/control/picker/picker.jsx"];
63153
+ import.meta.css = [css$u, "@jsenv/navi/src/control/picker/picker.jsx"];
63236
63154
  if (typeof props.maxLines === "string") {
63237
63155
  props.maxLines = parseInt(props.maxLines);
63238
63156
  }
@@ -63681,7 +63599,7 @@ installImportMetaCssBuild(import.meta);/**
63681
63599
  * refuse it on purpose, which is what keeps the focus where the travel happens
63682
63600
  * instead of moving it into a slide that is about to leave.
63683
63601
  */
63684
- const css$s = /* css */`
63602
+ const css$t = /* css */`
63685
63603
  @layer navi {
63686
63604
  .navi_picker_spin {
63687
63605
  /* A picker one steps through is still a picker: what themes every picker
@@ -64150,7 +64068,7 @@ const Spin = ({
64150
64068
  nextLabel,
64151
64069
  ...rest
64152
64070
  }) => {
64153
- import.meta.css = [css$s, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
64071
+ import.meta.css = [css$t, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
64154
64072
  const id = useId();
64155
64073
  // What the group around it says, when there is one: how big the whole thing
64156
64074
  // is written is said once, on the group, and every spin in it follows.
@@ -64686,7 +64604,7 @@ const renderValueDefault = value => String(value ?? "");
64686
64604
  * are passed on to the spins sitting in them.
64687
64605
  */
64688
64606
  const SpinGroup = props => {
64689
- import.meta.css = [css$s, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
64607
+ import.meta.css = [css$t, "@jsenv/navi/src/control/picker/picker_spin.jsx"];
64690
64608
  const {
64691
64609
  size
64692
64610
  } = props;
@@ -65199,7 +65117,7 @@ const TimeRangeSpin = ({
65199
65117
  };
65200
65118
 
65201
65119
  installImportMetaCssBuild(import.meta);// TOFIX: select in data then reset, it reset to red/blue instead of red/blue/green
65202
- const css$r = /* css */`
65120
+ const css$s = /* css */`
65203
65121
  .navi_checkbox_group {
65204
65122
  border-style: solid;
65205
65123
 
@@ -65240,7 +65158,7 @@ const CheckboxGroup = props => {
65240
65158
  return checkboxGroup;
65241
65159
  };
65242
65160
  const CheckboxGroupInterface = props => {
65243
- import.meta.css = [css$r, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
65161
+ import.meta.css = [css$s, "@jsenv/navi/src/control/input/checkbox_group.jsx"];
65244
65162
  const {
65245
65163
  ref
65246
65164
  } = props;
@@ -65289,7 +65207,7 @@ installImportMetaCssBuild(import.meta);/**
65289
65207
  * shared sheet is registered here too — a page may render a Textarea without
65290
65208
  * any Input.
65291
65209
  */
65292
- const css$q = /* css */`
65210
+ const css$r = /* css */`
65293
65211
  .navi_input.navi_textarea {
65294
65212
  .navi_control_input {
65295
65213
  min-height: calc(var(--textarea-min-rows, 1.5) * 1lh);
@@ -65376,7 +65294,7 @@ const Textarea = ({
65376
65294
  width = "35ch",
65377
65295
  ...props
65378
65296
  }) => {
65379
- import.meta.css = [inputCss + css$q, "@jsenv/navi/src/control/input/textarea.jsx"];
65297
+ import.meta.css = [inputCss + css$r, "@jsenv/navi/src/control/input/textarea.jsx"];
65380
65298
  const defaultRef = useRef(null);
65381
65299
  props.ref = props.ref || defaultRef;
65382
65300
  usePlaceholderHeight(props.ref, props.placeholder);
@@ -65450,7 +65368,7 @@ const TextareaCharCount = ({
65450
65368
  maxLength,
65451
65369
  ...rest
65452
65370
  }) => {
65453
- import.meta.css = [css$q, "@jsenv/navi/src/control/input/textarea.jsx"];
65371
+ import.meta.css = [css$r, "@jsenv/navi/src/control/input/textarea.jsx"];
65454
65372
  const resolvedValue = signal ? signal.value : value;
65455
65373
  const length = typeof resolvedValue === "string" ? resolvedValue.length : 0;
65456
65374
  return jsx(Box, {
@@ -65635,7 +65553,7 @@ const formatIntlUnit = (unit, {
65635
65553
  }
65636
65554
  };
65637
65555
 
65638
- installImportMetaCssBuild(import.meta);const css$p = /* css */`
65556
+ installImportMetaCssBuild(import.meta);const css$q = /* css */`
65639
65557
  .navi_input_duration {
65640
65558
  --duration-separator-spacing: 4px;
65641
65559
  --loader-color: var(--navi-loader-color);
@@ -65702,7 +65620,7 @@ installImportMetaCssBuild(import.meta);const css$p = /* css */`
65702
65620
  * "auto" aligns each field toward its neighbouring separator (first→right, last→left, middle/solo→center).
65703
65621
  */
65704
65622
  const InputDuration = props => {
65705
- import.meta.css = [css$p, "@jsenv/navi/src/control/input/input_duration.jsx"];
65623
+ import.meta.css = [css$q, "@jsenv/navi/src/control/input/input_duration.jsx"];
65706
65624
  const defaultRef = useRef();
65707
65625
  props.ref = props.ref || defaultRef;
65708
65626
  props.max = props.max || "23h59";
@@ -66204,7 +66122,7 @@ const InputDurationPart = ({
66204
66122
  });
66205
66123
  };
66206
66124
 
66207
- installImportMetaCssBuild(import.meta);const css$o = /* css */`
66125
+ installImportMetaCssBuild(import.meta);const css$p = /* css */`
66208
66126
  .navi_radio_group {
66209
66127
  border-style: solid;
66210
66128
 
@@ -66224,7 +66142,7 @@ const RadioGroup = props => {
66224
66142
  return radioGroup;
66225
66143
  };
66226
66144
  const RadioGroupInterface = props => {
66227
- import.meta.css = [css$o, "@jsenv/navi/src/control/input/radio_group.jsx"];
66145
+ import.meta.css = [css$p, "@jsenv/navi/src/control/input/radio_group.jsx"];
66228
66146
  const {
66229
66147
  ref
66230
66148
  } = props;
@@ -66282,7 +66200,7 @@ installImportMetaCssBuild(import.meta);/**
66282
66200
  * control is drawn — the list it opens stays the platform's own, which is the
66283
66201
  * whole point of using a select.
66284
66202
  */
66285
- const css$n = /* css */`
66203
+ const css$o = /* css */`
66286
66204
  .navi_input.navi_select {
66287
66205
  .navi_control_input {
66288
66206
  /* Room for the chevron, which sits over the padding rather than beside
@@ -66337,7 +66255,7 @@ const Select = ({
66337
66255
  multiple,
66338
66256
  ...props
66339
66257
  }) => {
66340
- import.meta.css = [inputCss + css$n, "@jsenv/navi/src/control/input/select.jsx"];
66258
+ import.meta.css = [inputCss + css$o, "@jsenv/navi/src/control/input/select.jsx"];
66341
66259
  const defaultRef = useRef(null);
66342
66260
  props.ref = props.ref || defaultRef;
66343
66261
  seedDefaultValueFromSignal(props);
@@ -66405,7 +66323,7 @@ installImportMetaCssBuild(import.meta);/**
66405
66323
  * running: one loading outline around both, which is why each half is told
66406
66324
  * `loadingOutline={false}`.
66407
66325
  */
66408
- const css$m = /* css */`
66326
+ const css$n = /* css */`
66409
66327
  /* Around the pair rather than in it: the outline is drawn against this
66410
66328
  element and follows its corners, and a Group counts its own children to
66411
66329
  know which corners to square — an outline among them would be one of the
@@ -66517,7 +66435,7 @@ const css$m = /* css */`
66517
66435
  * Anything else lands on the split button's own box.
66518
66436
  */
66519
66437
  const SplitButton = props => {
66520
- import.meta.css = [css$m, "@jsenv/navi/src/control/input/split_button.jsx"];
66438
+ import.meta.css = [css$n, "@jsenv/navi/src/control/input/split_button.jsx"];
66521
66439
  const {
66522
66440
  options = [],
66523
66441
  value,
@@ -67168,7 +67086,7 @@ installImportMetaCssBuild(import.meta);/*
67168
67086
  * accessors (top/height vs left/width) chosen from `horizontal`, and the CSS has
67169
67087
  * a [data-horizontal] variant.
67170
67088
  */
67171
- const css$l = /* css */`
67089
+ const css$m = /* css */`
67172
67090
  .navi_wheel_container {
67173
67091
  /* Row size and emphasis band are read together: the band is what a value
67174
67092
  must cross to lose its emphasis, and the row is how far it has to travel
@@ -68290,7 +68208,7 @@ const useWheelKeyboard = ({
68290
68208
  }, [isHorizontal, interactive, isLoop]);
68291
68209
  };
68292
68210
  function WheelUI(props) {
68293
- import.meta.css = [css$l, "@jsenv/navi/src/control/wheel/wheel.jsx"];
68211
+ import.meta.css = [css$m, "@jsenv/navi/src/control/wheel/wheel.jsx"];
68294
68212
  const {
68295
68213
  ref,
68296
68214
  visibleCount = 3,
@@ -68691,6 +68609,16 @@ function WheelUI(props) {
68691
68609
  // uiAction; here we fire the action explicitly, once, on the settled value.
68692
68610
  const input = inputRef.current;
68693
68611
  if (input) {
68612
+ // Said out loud too, OUTSIDE the action pipeline: the action above goes
68613
+ // through the gates (validity included), and what listens to a settle
68614
+ // may be exactly the thing that RESTORES validity — a range's other
68615
+ // bound giving way (see TimeRangeWheel) cannot wait on a gate that
68616
+ // refuses invalid values. Bubbles, so a group holding this wheel hears
68617
+ // it without knowing where the wheel sits.
68618
+ dispatchPublicCustomEvent(input, "navi_wheel_settle", {
68619
+ value: trackedItemsRef.current[index].value,
68620
+ event: settleEvent
68621
+ });
68694
68622
  dispatchRequestAction(input, {
68695
68623
  event: settleEvent
68696
68624
  });
@@ -69199,7 +69127,7 @@ Wheel.Item = WheelItem;
69199
69127
  * @param {boolean} [props.zoom] - Enlarge the centered value of every wheel (see Wheel's zoom prop) with one prop for the whole group.
69200
69128
  */
69201
69129
  const WheelGroup = props => {
69202
- import.meta.css = [css$l, "@jsenv/navi/src/control/wheel/wheel.jsx"];
69130
+ import.meta.css = [css$m, "@jsenv/navi/src/control/wheel/wheel.jsx"];
69203
69131
  // WheelGroup IS a control group: it aggregates its named wheels ("hours",
69204
69132
  // "minutes"…) into one object value, so it can sit directly inside a Form or a
69205
69133
  // Picker with no extra <ControlGroup> wrapper. The wheel-specific presentation
@@ -69230,7 +69158,11 @@ const WheelGroup = props => {
69230
69158
  const [controlgroupRootProps, controlgroupProps, childrenWrapperProps] = useControlgroupProps(props, {
69231
69159
  allowCapture: true,
69232
69160
  wantRequesterButtonState: true,
69233
- controlType: "control_group",
69161
+ // Its own type, not the generic "control_group": the group IS one
69162
+ // control made of parts, and a wheel settling runs the group's own
69163
+ // action — the way a radio checking runs its group's (the auto group
69164
+ // action in control_hooks keys on this type).
69165
+ controlType: "wheel_group",
69234
69166
  stateType: "object",
69235
69167
  cascadeValidationToChildren: true,
69236
69168
  aggregateChildStates: props.aggregateChildStates,
@@ -69365,7 +69297,7 @@ const WheelColon = props => {
69365
69297
  };
69366
69298
  Wheel.Colon = WheelColon;
69367
69299
 
69368
- /**
69300
+ installImportMetaCssBuild(import.meta);/**
69369
69301
  * A time of day, and a span between two of them, set by turning rather than by
69370
69302
  * typing. A wheel only ever shows values that exist: there is no half-written
69371
69303
  * hour to bound and correct under the fingers, which is what a time typed digit
@@ -69375,11 +69307,33 @@ Wheel.Colon = WheelColon;
69375
69307
  * like `TimeSpin` — the two are interchangeable in a form. `TimeRangeWheel` is
69376
69308
  * two of those and carries `{ start, end }`, with the rule such a pair always
69377
69309
  * has: the end comes after the start. Here that rule is lived rather than
69378
- * checked — the bounds push each other while they turn, so what the wheels show
69379
- * is always a span. The send-time constraint stays underneath for what pushing
69380
- * cannot fix (a start so late the span no longer fits in the day).
69310
+ * checked — a bound settling pushes the other out of its way, so what the
69311
+ * wheels show at rest is always a span. The send-time constraint stays
69312
+ * underneath for what pushing cannot fix (a start so late the span no longer
69313
+ * fits in the day).
69381
69314
  */
69315
+ const css$l = /* css */`
69316
+ /* The words around a span's wheels ("De", "à"): one line box, the height
69317
+ of a wheel row (--wheel-item-height re-exposed, same value as
69318
+ .navi_wheel_container), centered against the wheels by the group. The
69319
+ box's strut — this element's own font — is what places the baseline,
69320
+ exactly where a wheel row places its numbers'; and because the content
69321
+ stays in inline flow, anything written INSIDE at another size still
69322
+ sits on that same baseline. Two things this depends on: the label is a
69323
+ <Text size={size}> so its em — and therefore this line-height — is the
69324
+ SAME em as the wheel rows' (a label left at the control font under
69325
+ bigger wheels computes a shorter row and its baseline drifts); and no
69326
+ flex centering of the content (centering re-centers a smaller glyph,
69327
+ baselines are not centers). */
69328
+ .navi_time_range_label {
69329
+ --wheel-item-height: round(1.8em, 1px);
69382
69330
 
69331
+ color: var(--wheel-color, light-dark(#111, #eee));
69332
+ line-height: var(--wheel-item-height);
69333
+ white-space: nowrap;
69334
+ user-select: none;
69335
+ }
69336
+ `;
69383
69337
  const HOUR_COUNT = 24;
69384
69338
  const MINUTES_PER_HOUR = 60;
69385
69339
  const LAST_MINUTE_OF_DAY = 23 * 60 + 59;
@@ -69538,6 +69492,7 @@ const TimeRangeWheel = ({
69538
69492
  endTimeProps,
69539
69493
  ...rest
69540
69494
  }) => {
69495
+ import.meta.css = [css$l, "@jsenv/navi/src/control/wheel/wheel_time.jsx"];
69541
69496
  const startId = useId();
69542
69497
  const startRef = useRef(null);
69543
69498
  const endRef = useRef(null);
@@ -69549,10 +69504,15 @@ const TimeRangeWheel = ({
69549
69504
  distributeChildUIState
69550
69505
  } = useAnswered(placeholder, rest, aggregateSpan, distributeSpan);
69551
69506
 
69552
- // What the pair does while it is being turned: the bound that just moved is
69553
- // the one the user is holding, so it stays where it was put and the OTHER one
69554
- // gives way. A refusal at the end of the gesture would leave the person to
69555
- // undo what they just did.
69507
+ // What the pair does once a bound SETTLES: the one that was moved stays
69508
+ // where it was put and the OTHER one gives way a refusal at that point
69509
+ // would leave the person to undo what they just did. Settles, not while it
69510
+ // turns: a wheel under the finger holds a value nobody has chosen yet, and
69511
+ // the other bound jumping around mid-gesture answers a question that was
69512
+ // not asked. Wired on the wheel's settle EVENT (navi_wheel_settle), never
69513
+ // on `action`: an action goes through the gates, validity included, and
69514
+ // the moment this must run is precisely the moment the pair is INVALID —
69515
+ // an action-gated push would be refused by the very thing it fixes.
69556
69516
  const keepBoundsApart = (movedSide, movedTime, e) => {
69557
69517
  const movedMinutes = minutesFromTime(movedTime);
69558
69518
  if (movedMinutes === null) {
@@ -69596,6 +69556,7 @@ const TimeRangeWheel = ({
69596
69556
  value: answeredRef,
69597
69557
  children: [startLabel === null ? null : jsx(Text, {
69598
69558
  size: size,
69559
+ className: "navi_time_range_label",
69599
69560
  children: startLabel
69600
69561
  }), jsx(TimeWheel, {
69601
69562
  id: startId,
@@ -69605,12 +69566,20 @@ const TimeRangeWheel = ({
69605
69566
  hours: hours,
69606
69567
  loop: loop,
69607
69568
  size: size,
69608
- placeholder: placeholder ? placeholder.start : undefined,
69609
- uiAction: (value, e) => keepBoundsApart("start", value, e),
69569
+ placeholder: placeholder ? placeholder.start : undefined
69570
+ // e.detail.value is the settled WHEEL's own value (an hour, a
69571
+ // minute) — half a time. What the pair compares is this bound's
69572
+ // whole time, read off the element the listener sits on.
69573
+ ,
69574
+
69575
+ onnavi_wheel_settle: e => {
69576
+ keepBoundsApart("start", getUIStateFromElement(e.currentTarget), e);
69577
+ },
69610
69578
  ...timeProps,
69611
69579
  ...startTimeProps
69612
69580
  }), endLabel === null ? null : jsx(Text, {
69613
69581
  size: size,
69582
+ className: "navi_time_range_label",
69614
69583
  children: endLabel
69615
69584
  }), jsx(TimeWheel, {
69616
69585
  ref: endRef,
@@ -69620,11 +69589,14 @@ const TimeRangeWheel = ({
69620
69589
  loop: loop,
69621
69590
  size: size,
69622
69591
  placeholder: placeholder ? placeholder.end : undefined,
69623
- uiAction: (value, e) => keepBoundsApart("end", value, e)
69592
+ onnavi_wheel_settle: e => {
69593
+ keepBoundsApart("end", getUIStateFromElement(e.currentTarget), e);
69594
+ }
69624
69595
  // Which time it comes after, and how much room there must be between
69625
69596
  // the two: said on the LATER of the two, so the answer is given where
69626
69597
  // the time one would have to move is (see time_range_constraint.js).
69627
69598
  ,
69599
+
69628
69600
  "data-time-after": startId,
69629
69601
  "data-time-min-duration": minDuration,
69630
69602
  ...timeProps,
@@ -75257,18 +75229,34 @@ installImportMetaCssBuild(import.meta);/**
75257
75229
  * Two distinct facts, drawn separately — they usually agree, and everything
75258
75230
  * this component says comes from the moments they do not:
75259
75231
  *
75260
- * - the PATH (`reached`): how far the steps are answered without a gap. A
75261
- * solid line runs from the first dot to the step it names, and those dots
75262
- * are filled; past it the line is dashed the road not walked yet. It
75263
- * moves when a step is answered, not when one merely looks around.
75232
+ * - DONE and the PATH (the blue fill). The mental model, to keep in mind
75233
+ * when touching any of this: the circles are THINGS TO DO, and the path is
75234
+ * the progression along the linear walk from the first to the last — a
75235
+ * thing done is what lets the path advance. Answering a step fills its
75236
+ * dot AND the line onward, up to the NEXT dot: the invitation to go
75237
+ * there. If that next step is already done the path crosses it from
75238
+ * behind and carries on, and so forth — the fill covers the answered
75239
+ * prefix plus one segment of appetite. It stops at the edge of the first
75240
+ * dot not answered, which stays empty: the fill's one meaning is
75241
+ * "answered", and the first dot is NOT filled on arrival — it becomes so
75242
+ * by being answered. Past the fill the line is dashed, the road not
75243
+ * walked yet; steps answered out of order are filled dots standing alone,
75244
+ * dashed segments around them: the holes, readable at a glance.
75264
75245
  * - the POSITION (`current`): the step being looked at, marked by a halo
75265
- * around its dot and its label emphasized. It travels freely, so it can be
75266
- * AHEAD of the path (browsing step 3 while step 1 still misses an answer)
75267
- * or BEHIND it (back on step 1 to change something already answered).
75246
+ * around its dot and its label emphasized. It travels freely, so it can
75247
+ * be AHEAD of the path or BEHIND it.
75268
75248
  *
75269
75249
  * Both move smoothly on change (a CSS transition each), so pressing a step
75270
75250
  * or answering one is seen travelling rather than jumping.
75271
75251
  *
75252
+ * The steps are the CHILDREN — <StepList.Item value="club">Club</StepList.Item>.
75253
+ * An Item renders nothing: it REGISTERS with the list as it renders (order
75254
+ * of rendering is the order of the steps), and the list draws everything —
75255
+ * which is what lets an Item come from anywhere: a .map(), a fragment, a
75256
+ * component of your own wrapping it. One caveat comes with reading the
75257
+ * children as they render: hand the list fresh Item vnodes on each render
75258
+ * (the usual JSX), not a memoized array a bailout would keep from rendering.
75259
+ *
75272
75260
  * The dots are drawn in SVG, twice: a muted layer, and a filled layer
75273
75261
  * clipped at the path's edge — the clip is what makes the path's progress a
75274
75262
  * single sweep that fills the line and the dots it crosses in one movement.
@@ -75277,28 +75265,21 @@ installImportMetaCssBuild(import.meta);/**
75277
75265
  * surface. Colors are CSS custom properties (see the css below), overridden
75278
75266
  * from outside for a dark band or a different accent.
75279
75267
  *
75280
- * The steps are the CHILDREN — <StepList.Item value="club">Club</StepList.Item>
75281
- * — read off the vnodes (toChildArray, so a .map() or a fragment is fine; a
75282
- * component of your own wrapping an Item is not seen). The Item renders its
75283
- * label; everything positional is this component's business.
75284
- *
75285
75268
  * `slideContainer` connects the list to a <SlideContainer> by id, both ways:
75286
75269
  * pressing a step travels there (--navi-go-to-slide), and the position is
75287
75270
  * READ off the container rather than said by a prop — including mid-travel:
75288
75271
  * the container paints --slide-travel-progress on this element (it is a
75289
75272
  * follower, same mechanism as <Nav slideContainer>), so the halo rides the
75290
- * drag under the finger, in CSS alone. The path then follows the position
75291
- * too, clamped between `reached` (never retracts) and `reachable` (never
75292
- * ahead of the answers): dragging towards a step whose way is earned fills
75293
- * the line under the finger, dragging past the answers does not.
75273
+ * drag under the finger, in CSS alone. The path is not concerned: it moves
75274
+ * on answers, never on movement.
75294
75275
  */
75295
75276
  const css$2 = /* css */`
75296
75277
  .navi_step_list {
75297
- /* The knobs: accent is the path, muted is what the path has not
75298
- reached, on-accent writes on filled dots, and --step-list-path-line
75299
- exists apart from the accent for a dark band where the walked line
75300
- reads better plain white. Said from OUTSIDE (any ancestor — a dark
75301
- band, a themed app) on the plain names; resolved here through an
75278
+ /* The knobs: one accent for everything filled the dots and the line
75279
+ share it, because the fill has ONE meaning (answered) and a meaning
75280
+ does not change color. Muted is what is not answered, on-accent
75281
+ writes on filled dots. Said from OUTSIDE (any ancestor — a dark band,
75282
+ a themed app) on the plain names; resolved here through an
75302
75283
  indirection (--x-…, the way Button does), because a default written
75303
75284
  on the plain name on this very element would beat anything an
75304
75285
  ancestor says. */
@@ -75313,10 +75294,9 @@ const css$2 = /* css */`
75313
75294
  --step-list-current-color,
75314
75295
  light-dark(#1c2433, white)
75315
75296
  );
75316
- --x-step-list-path-line: var(
75317
- --step-list-path-line,
75318
- var(--x-step-list-accent)
75319
- );
75297
+ /* How long a movement takes — the path sweeping, the halo sliding. One
75298
+ number for all of them: they tell one story. */
75299
+ --x-step-list-duration: var(--step-list-duration, 300ms);
75320
75300
 
75321
75301
  position: relative;
75322
75302
  display: block;
@@ -75354,14 +75334,26 @@ const css$2 = /* css */`
75354
75334
  .navi_step_list_rail g[data-current] text {
75355
75335
  fill: var(--x-step-list-current-color);
75356
75336
  }
75357
- /* The path: same drawing, filled, revealed up to the step it has come to.
75358
- The clip is set inline (a width in px); transitioning it is what makes
75359
- an answered step SWEEP the line and the next dot rather than pop. */
75337
+ /* A step answered has its dot filled, wherever the path stands: answered
75338
+ out of order it stands alone, a filled dot between dashed segments.
75339
+ After the current rule on purpose: answered wins the drawing, the halo
75340
+ says current. */
75341
+ .navi_step_list_rail g[data-done] circle {
75342
+ fill: var(--x-step-list-accent);
75343
+ stroke: var(--x-step-list-accent);
75344
+ }
75345
+ .navi_step_list_rail g[data-done] text {
75346
+ fill: var(--x-step-list-on-accent);
75347
+ }
75348
+ /* The path: same drawing, filled, revealed up to the fill's edge — the
75349
+ answered prefix plus its segment of appetite (see the top comment). The
75350
+ clip is set inline (a width in px); transitioning it is what makes an
75351
+ answered step SWEEP its dot and the line onward rather than pop. */
75360
75352
  .navi_step_list_rail_filled {
75361
- transition: clip-path 300ms ease;
75353
+ transition: clip-path var(--x-step-list-duration) ease;
75362
75354
  }
75363
75355
  .navi_step_list_rail_filled line {
75364
- stroke: var(--x-step-list-path-line);
75356
+ stroke: var(--x-step-list-accent);
75365
75357
  stroke-dasharray: none;
75366
75358
  }
75367
75359
  .navi_step_list_rail_filled circle {
@@ -75375,7 +75367,7 @@ const css$2 = /* css */`
75375
75367
  to dot (transform, transitioned) — the g moves, the circle inside is
75376
75368
  drawn at x=0. */
75377
75369
  .navi_step_list_marker {
75378
- transition: transform 300ms ease;
75370
+ transition: transform var(--x-step-list-duration) ease;
75379
75371
  }
75380
75372
  .navi_step_list_marker circle {
75381
75373
  fill: none;
@@ -75387,8 +75379,7 @@ const css$2 = /* css */`
75387
75379
  container paints --slide-travel-progress here (this element follows it,
75388
75380
  see data-slide-container-follows) — an asked-for travel animates it, a
75389
75381
  finger drags it — and everything below is a calc() of that number, so
75390
- the halo and the path move per frame in CSS alone. The transitions are
75391
- off: they would chase a finger that is already the pace.
75382
+ the halo and the path move per frame in CSS alone.
75392
75383
  Position, in dots-x px: where the picture is right now. */
75393
75384
  .navi_step_list[data-slide-container-follows] {
75394
75385
  --step-list-position: calc(
@@ -75400,36 +75391,11 @@ const css$2 = /* css */`
75400
75391
  transform: translateX(calc(var(--step-list-position) * 1px));
75401
75392
  transition: none;
75402
75393
  }
75403
- /* The path follows the position, clamped: never back below what was
75404
- earned (--step-list-reached-x), never ahead of what the answers allow
75405
- (--step-list-reachable-x). The +14 covers the dot it stands on (radius
75406
- plus stroke). */
75407
- .navi_step_list[data-slide-container-follows] .navi_step_list_rail_filled {
75408
- clip-path: inset(
75409
- 0
75410
- calc(
75411
- (
75412
- var(--step-list-w, 0) -
75413
- (
75414
- clamp(
75415
- var(--step-list-reached-x, -9999),
75416
- var(--step-list-position),
75417
- var(--step-list-reachable-x, -9999)
75418
- ) +
75419
- 14
75420
- )
75421
- ) *
75422
- 1px
75423
- )
75424
- 0 0
75425
- );
75426
- transition: none;
75427
- }
75428
75394
 
75429
75395
  /* One press target per step, covering the dot AND the label under it. The
75430
75396
  feedback is NOT the whole surface: a rectangle would say the whole band
75431
75397
  is a button, when the affordance is the dot — so hover and focus land on
75432
- a circle drawn over the dot (::before), plus the label brightening.
75398
+ a circle drawn over the dot, plus the label brightening.
75433
75399
  --step-dot-x anchors both on the dot, wherever the dot sits in the slot:
75434
75400
  the first and last slots are asymmetric (cut at the container's edge,
75435
75401
  see the geometry in the component). */
@@ -75454,13 +75420,15 @@ const css$2 = /* css */`
75454
75420
  --button-color-readonly: var(--x-step-list-muted);
75455
75421
  /* The button's own focus ring, silenced: it would outline the whole
75456
75422
  press surface, and the ring this list draws is the one around the dot
75457
- (see the ::before rules below) — two rings read as a mistake. Width
75458
- rather than style, because the ::before sets its own style in full. */
75423
+ (see below) — two rings read as a mistake. Width rather than style,
75424
+ because the dot sets its own style in full. */
75459
75425
  --button-outline-width: 0px;
75460
75426
  }
75461
75427
  /* Centered on the dot: same vertical middle as the rail (top 0, height 34,
75462
- cy 17). */
75463
- .navi_step_list_step::before {
75428
+ cy 17). A real element rather than a ::before, because it is also what a
75429
+ callout anchors to (data-callout-anchor needs a selector) — a message
75430
+ about a step points at its CIRCLE, not at the press surface. */
75431
+ .navi_step_list_dot {
75464
75432
  position: absolute;
75465
75433
  top: 17px;
75466
75434
  left: var(--step-dot-x);
@@ -75468,18 +75436,18 @@ const css$2 = /* css */`
75468
75436
  height: 30px;
75469
75437
  border-radius: 50%;
75470
75438
  translate: -50% -50%;
75471
- content: "";
75439
+ pointer-events: none;
75472
75440
  }
75473
- .navi_step_list_step:hover::before,
75474
- .navi_step_list_step[data-hover]::before {
75441
+ .navi_step_list_step:hover .navi_step_list_dot,
75442
+ .navi_step_list_step[data-hover] .navi_step_list_dot {
75475
75443
  background: color-mix(in srgb, var(--x-step-list-accent) 15%, transparent);
75476
75444
  }
75477
75445
  .navi_step_list .navi_step_list_step:hover,
75478
75446
  .navi_step_list .navi_step_list_step[data-hover] {
75479
75447
  --button-color: var(--x-step-list-current-color);
75480
75448
  }
75481
- .navi_step_list_step:focus-visible::before,
75482
- .navi_step_list_step[data-focus-visible]::before {
75449
+ .navi_step_list_step:focus-visible .navi_step_list_dot,
75450
+ .navi_step_list_step[data-focus-visible] .navi_step_list_dot {
75483
75451
  outline-width: var(--navi-focus-outline-width);
75484
75452
  outline-style: solid;
75485
75453
  outline-color: var(--navi-focus-outline-color);
@@ -75507,28 +75475,24 @@ const EDGE_INSET = 30;
75507
75475
  // halo of a current dot not to sit on the line.
75508
75476
  const LINE_GAP = 5;
75509
75477
 
75478
+ // What the Items say to the list holding them (see Step): where to write
75479
+ // themselves down. Null outside any list — a Step alone renders nothing and
75480
+ // registers nowhere.
75481
+ const StepListContext = createContext(null);
75482
+
75510
75483
  /**
75511
75484
  * @type {import("ignore:preact").FunctionComponent<{
75512
75485
  * current?: string,
75513
- * reached?: string,
75514
- * reachable?: string,
75515
75486
  * slideContainer?: string,
75516
75487
  * travelByClick?: boolean,
75517
75488
  * travelByKeyboard?: boolean,
75489
+ * duration?: string,
75518
75490
  * [key: string]: any,
75519
75491
  * }>}
75520
75492
  * @param {string} [current] - the step being looked at: its dot gets the
75521
75493
  * halo, its label the emphasis. Omit for "nowhere" — a confirmation
75522
75494
  * screen after the walk, say. With `slideContainer` the position is read
75523
75495
  * off the container instead, and this prop is ignored.
75524
- * @param {string} [reached] - the step the path has come to: the line is
75525
- * solid and the dots filled up to it, dashed past it. Omit for a path
75526
- * that has not started.
75527
- * @param {string} [reachable] - how far the path MAY go (with
75528
- * `slideContainer` only): between `reached` and this step the path
75529
- * follows the position — a drag towards a step whose way is earned fills
75530
- * the line under the finger. Defaults to `reached`: the path then never
75531
- * moves with the position at all.
75532
75496
  * @param {string} [slideContainer] - id of a <SlideContainer> these steps
75533
75497
  * are the slides of. Pressing a step travels there
75534
75498
  * (--navi-go-to-slide), the halo follows the container — drags included —
@@ -75545,14 +75509,16 @@ const LINE_GAP = 5;
75545
75509
  * CONTAINER — this element is a follower, so a press here already walks
75546
75510
  * the slides, and the container's own `travelByKeyboard` is the one that
75547
75511
  * says so. One owner per mode, or one arrow would do both.
75512
+ * @param {string} [duration] - how long a movement takes (the path
75513
+ * sweeping, the halo sliding), any CSS duration. 300ms unless said —
75514
+ * here, or from outside via --step-list-duration.
75548
75515
  */
75549
75516
  const StepList = ({
75550
75517
  current,
75551
- reached,
75552
- reachable,
75553
75518
  slideContainer,
75554
75519
  travelByClick = true,
75555
75520
  travelByKeyboard = true,
75521
+ duration,
75556
75522
  children,
75557
75523
  ...rest
75558
75524
  }) => {
@@ -75584,11 +75550,33 @@ const StepList = ({
75584
75550
  direction: "x"
75585
75551
  });
75586
75552
 
75587
- // The steps, read off the children: each <StepList.Item> vnode says which
75588
- // step it is (value) and is rendered as the label under its dot.
75589
- const stepVNodes = toChildArray(children).filter(child => child && child.props);
75590
- const stepCount = stepVNodes.length;
75591
- const valueOf = (vnode, index) => vnode.props.value ?? String(index);
75553
+ // The roll call: every render of this list opens a fresh page, the Items
75554
+ // rendering below write themselves on it (in rendering order, which is the
75555
+ // order of the steps), and the layout effect reads the page back. What was
75556
+ // read is STATE — the first render knows no steps, the effect's render
75557
+ // draws them and a change in what the Items say (a done toggled) flows
75558
+ // the same way: fresh page, fresh read, redraw.
75559
+ const registryRef = useRef(null);
75560
+ if (!registryRef.current) {
75561
+ registryRef.current = {
75562
+ renderedSteps: []
75563
+ };
75564
+ }
75565
+ const registry = registryRef.current;
75566
+ registry.renderedSteps = [];
75567
+ const [steps, setSteps] = useState([]);
75568
+ useLayoutEffect(() => {
75569
+ const collected = registry.renderedSteps;
75570
+ // An empty page while steps are known: the children were most likely
75571
+ // bailed out of rendering (memoized vnodes), not removed — keeping the
75572
+ // known steps beats erasing the drawing (see the caveat in the top
75573
+ // comment).
75574
+ if (collected.length === 0 && steps.length > 0) {
75575
+ return;
75576
+ }
75577
+ setSteps(previous => sameSteps(previous, collected) ? previous : [...collected]);
75578
+ });
75579
+ const stepCount = steps.length;
75592
75580
  const dotXs = [];
75593
75581
  if (width > 0 && stepCount > 0) {
75594
75582
  const span = width - EDGE_INSET * 2;
@@ -75598,7 +75586,7 @@ const StepList = ({
75598
75586
  index++;
75599
75587
  }
75600
75588
  }
75601
- const indexOf = value => stepVNodes.findIndex((vnode, index) => valueOf(vnode, index) === value);
75589
+ const indexOf = value => steps.findIndex(step => step.value === value);
75602
75590
 
75603
75591
  // Where the slides are, read off the container: which slide is current,
75604
75592
  // and — while a travel or a drag is playing — which one the picture leans
@@ -75656,19 +75644,31 @@ const StepList = ({
75656
75644
  }, [slideContainer, width, stepCount]);
75657
75645
  const resolvedCurrent = slideContainer ? containerCurrent : current;
75658
75646
  const currentIndex = resolvedCurrent === undefined ? -1 : indexOf(resolvedCurrent);
75659
- const reachedIndex = reached === undefined ? -1 : indexOf(reached);
75660
- let reachableIndex = reachable === undefined ? -1 : indexOf(reachable);
75661
- if (reachableIndex < reachedIndex) {
75662
- reachableIndex = reachedIndex;
75663
- }
75664
- // Covers the reached dot entirely (radius plus stroke), and nothing when
75665
- // the path has not started.
75666
- const fillX = reachedIndex === -1 ? 0 : dotXs[reachedIndex] + DOT_R + 3;
75647
+ // The path, deduced from what the Items say: the steps answered without a
75648
+ // gap from the start. -1 when the first step is not answered yet — there
75649
+ // is no path then, only dots.
75650
+ let pathEndIndex = steps.findIndex(step => !step.done);
75651
+ if (pathEndIndex === -1) {
75652
+ pathEndIndex = stepCount;
75653
+ }
75654
+ pathEndIndex -= 1;
75655
+ // How far the fill goes: nowhere while nothing is answered; past the last
75656
+ // dot (radius plus stroke) when everything is; otherwise THROUGH the
75657
+ // answered prefix and onward to the edge of the next dot — the segment of
75658
+ // appetite (see the top comment), with the dot it points at left empty.
75659
+ let fillX;
75660
+ if (pathEndIndex === -1) {
75661
+ fillX = 0;
75662
+ } else if (pathEndIndex >= stepCount - 1) {
75663
+ fillX = dotXs[stepCount - 1] + DOT_R + 3;
75664
+ } else {
75665
+ fillX = dotXs[pathEndIndex + 1] - DOT_R - LINE_GAP;
75666
+ }
75667
75667
  const cy = RAIL_H / 2;
75668
75668
  const slotWidth = dotXs.length > 1 ? dotXs[1] - dotXs[0] : width;
75669
75669
  const renderRail = filled => jsx("svg", {
75670
75670
  className: filled ? "navi_step_list_rail navi_step_list_rail_filled" : "navi_step_list_rail",
75671
- style: filled && !slideContainer ? {
75671
+ style: filled ? {
75672
75672
  clipPath: `inset(0 ${width - fillX}px 0 0)`
75673
75673
  } : undefined,
75674
75674
  width: width,
@@ -75676,9 +75676,10 @@ const StepList = ({
75676
75676
  viewBox: `0 0 ${width} ${RAIL_H}`,
75677
75677
  "aria-hidden": "true",
75678
75678
  children: dotXs.map((x, index) => jsxs("g", {
75679
- // Base layer only: a current dot the path covers keeps the filled
75680
- // colors (see the css).
75679
+ // Base layer only: dots the path covers are drawn filled by the
75680
+ // layer above anyway (see the css).
75681
75681
  "data-current": !filled && index === currentIndex ? "" : undefined,
75682
+ "data-done": !filled && steps[index].done ? "" : undefined,
75682
75683
  children: [index > 0 ? jsx("line", {
75683
75684
  x1: dotXs[index - 1] + DOT_R + LINE_GAP,
75684
75685
  y1: cy,
@@ -75695,9 +75696,9 @@ const StepList = ({
75695
75696
  "text-anchor": "middle",
75696
75697
  children: index + 1
75697
75698
  })]
75698
- }, valueOf(stepVNodes[index], index)))
75699
+ }, steps[index].value))
75699
75700
  });
75700
- return jsx(Box, {
75701
+ return jsxs(Box, {
75701
75702
  ...rest,
75702
75703
  ref: rootRef,
75703
75704
  baseClassName: "navi_step_list",
@@ -75710,13 +75711,14 @@ const StepList = ({
75710
75711
  "data-slide-container-follows": slideContainer,
75711
75712
  style: {
75712
75713
  ...rest.style,
75713
- ...(slideContainer ? {
75714
- "--step-list-w": width,
75715
- "--step-list-reached-x": reachedIndex === -1 ? -9999 : dotXs[reachedIndex],
75716
- "--step-list-reachable-x": reachableIndex === -1 ? -9999 : dotXs[reachableIndex]
75714
+ ...(duration ? {
75715
+ "--step-list-duration": duration
75717
75716
  } : undefined)
75718
75717
  },
75719
- children: width > 0 && stepCount > 0 ? jsxs(Fragment$1, {
75718
+ children: [jsx(StepListContext.Provider, {
75719
+ value: registry,
75720
+ children: children
75721
+ }), width > 0 && stepCount > 0 ? jsxs(Fragment$1, {
75720
75722
  children: [renderRail(false), renderRail(true), currentIndex !== -1 && dotXs[currentIndex] !== undefined ? jsx("svg", {
75721
75723
  className: "navi_step_list_rail",
75722
75724
  width: width,
@@ -75738,15 +75740,7 @@ const StepList = ({
75738
75740
  r: RING_R
75739
75741
  })
75740
75742
  })
75741
- }) : null, stepVNodes.map((stepVNode, index) => {
75742
- const value = valueOf(stepVNode, index);
75743
- // Whatever else the Item was given reaches its button — a
75744
- // pseudoState held for a demo, an aria attribute.
75745
- const itemRest = {
75746
- ...stepVNode.props
75747
- };
75748
- delete itemRest.value;
75749
- delete itemRest.children;
75743
+ }) : null, steps.map((step, index) => {
75750
75744
  // The slots tile the row, cut at the container's edges: the
75751
75745
  // first and the last cover only the inner half of the room an
75752
75746
  // interior slot gets, so pressing just outside the box presses
@@ -75762,8 +75756,8 @@ const StepList = ({
75762
75756
  "width": `${slotRight - slotLeft}px`,
75763
75757
  "--step-dot-x": `${dotXs[index] - slotLeft}px`
75764
75758
  },
75765
- children: jsx(Button, {
75766
- ...itemRest,
75759
+ children: jsxs(Button, {
75760
+ ...step.buttonProps,
75767
75761
  // bare, not discrete: what is drawn IS the dot and its
75768
75762
  // label — the hover wash a discrete button paints over its
75769
75763
  // whole surface is exactly what must not appear here (the
@@ -75776,38 +75770,105 @@ const StepList = ({
75776
75770
  // Towards the slides when connected, by name: the command
75777
75771
  // reaches the container wherever this list sits on the
75778
75772
  // page. What else a press should do is the Item's own
75779
- // onClick, which arrived through itemRest.
75773
+ // onClick, which arrived through buttonProps.
75774
+ ,
75775
+
75776
+ command: slideContainer && travelByClick ? `--navi-go-to-slide:${step.value}` : undefined,
75777
+ commandFor: slideContainer
75778
+ // A message about a step (a callout) points at its circle,
75779
+ // above it: the label lives below.
75780
75780
  ,
75781
75781
 
75782
- command: slideContainer && travelByClick ? `--navi-go-to-slide:${value}` : undefined,
75783
- commandFor: slideContainer,
75784
- children: jsx("span", {
75782
+ "data-callout-anchor": ".navi_step_list_dot",
75783
+ "data-callout-position": "top",
75784
+ children: [jsx("span", {
75785
+ className: "navi_step_list_dot",
75786
+ "aria-hidden": "true"
75787
+ }), jsx("span", {
75785
75788
  className: "navi_step_list_label",
75786
- children: stepVNode
75787
- })
75789
+ children: step.label
75790
+ })]
75788
75791
  })
75789
- }, value);
75792
+ }, step.value);
75790
75793
  })]
75791
- }) : null
75794
+ }) : null]
75792
75795
  });
75793
75796
  };
75794
75797
 
75798
+ // The same steps saying the same things: nothing to redraw. The labels are
75799
+ // vnodes, fresh objects on every render — comparing them would always say
75800
+ // "changed", so they are left out: what they show changes through the state
75801
+ // it came from, which re-renders this list anyway.
75802
+ const sameSteps = (previousSteps, nextSteps) => {
75803
+ if (previousSteps.length !== nextSteps.length) {
75804
+ return false;
75805
+ }
75806
+ let index = 0;
75807
+ while (index < previousSteps.length) {
75808
+ const previous = previousSteps[index];
75809
+ const next = nextSteps[index];
75810
+ if (previous.value !== next.value || previous.done !== next.done) {
75811
+ return false;
75812
+ }
75813
+ if (!sameShallow(previous.buttonProps, next.buttonProps)) {
75814
+ return false;
75815
+ }
75816
+ index++;
75817
+ }
75818
+ return true;
75819
+ };
75820
+ const sameShallow = (previousObject, nextObject) => {
75821
+ const previousKeys = Object.keys(previousObject);
75822
+ const nextKeys = Object.keys(nextObject);
75823
+ if (previousKeys.length !== nextKeys.length) {
75824
+ return false;
75825
+ }
75826
+ for (const key of previousKeys) {
75827
+ if (previousObject[key] !== nextObject[key]) {
75828
+ return false;
75829
+ }
75830
+ }
75831
+ return true;
75832
+ };
75833
+
75795
75834
  /**
75796
- * One step of the walk: `value` names it (what `current`/`reached` say and
75797
- * what a press reports), the children are its label. Rendered under its dot;
75798
- * where the dot is, and what state it shows, is the StepList's business.
75835
+ * One step of the walk: `value` names it (what `current` says and what a
75836
+ * press reports), `done` says it is answered (its dot fills, and the path is
75837
+ * deduced from the answered steps), the children are its label.
75838
+ *
75839
+ * It renders NOTHING: it registers with the list around it as it renders,
75840
+ * and the list draws everything — the dot, the label, the button. Whatever
75841
+ * else it carries (onClick, pseudoState, aria-*) lands on that button.
75799
75842
  *
75800
75843
  * It is both StepList.Item and an export of its own, the way Slide is to
75801
75844
  * SlideContainer.
75802
75845
  *
75803
75846
  * @type {import("ignore:preact").FunctionComponent<{
75804
75847
  * value: string,
75848
+ * done?: boolean,
75805
75849
  * [key: string]: any,
75806
75850
  * }>}
75851
+ * @param {boolean} [done] - this step is answered: its dot is filled — the
75852
+ * fill's one meaning. Steps answered out of order are filled dots
75853
+ * standing alone, dashed segments around them.
75807
75854
  */
75808
75855
  const Step = ({
75809
- children
75810
- }) => children;
75856
+ value,
75857
+ done,
75858
+ children,
75859
+ ...buttonProps
75860
+ }) => {
75861
+ const registry = useContext(StepListContext);
75862
+ if (registry) {
75863
+ registry.renderedSteps.push({
75864
+ value: value ?? String(registry.renderedSteps.length),
75865
+ done: Boolean(done),
75866
+ label: children,
75867
+ buttonProps
75868
+ });
75869
+ }
75870
+ return null;
75871
+ };
75811
75872
  StepList.Item = Step;
75812
75873
 
75813
75874
  installImportMetaCssBuild(import.meta);const css$1 = /* css */`