@jsenv/navi 0.29.233 → 0.29.235

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.
@@ -1952,6 +1952,8 @@ const stringifyForDisplay = (
1952
1952
  return String(value);
1953
1953
  };
1954
1954
 
1955
+ const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
1956
+
1955
1957
  /*
1956
1958
  * Deep structural equality for arbitrary JS values — what `===` can't do but this
1957
1959
  * codebase constantly needs: memoization cache keys ({ id: 1 } equal to { id: 1 }),
@@ -2060,6 +2062,21 @@ const compareTwoJsValues = (
2060
2062
  return result;
2061
2063
  };
2062
2064
  const compareComposite = (a, b) => {
2065
+ // An object carrying its signal (a resource relation read through
2066
+ // `.one()` or `.many()`) is that signal in the costume of its current
2067
+ // value: it says "follow this", not "this". Two of them are the same
2068
+ // thing when they follow the same signal, whatever they show right now —
2069
+ // and neither is ever a plain value that happens to look alike. Without
2070
+ // this a params object bound to the relation and one holding the row as
2071
+ // data would pass for equal, and the binding would be handed back for the
2072
+ // row (see bindParams' child cache in actions.js). Decided ahead of
2073
+ // SYMBOL_IDENTITY: the costume is a copy of the row and carries its
2074
+ // identity, which is exactly the sameness that must not count here.
2075
+ const aObjectSignal = a[SYMBOL_OBJECT_SIGNAL];
2076
+ const bObjectSignal = b[SYMBOL_OBJECT_SIGNAL];
2077
+ if (aObjectSignal || bObjectSignal) {
2078
+ return aObjectSignal === bObjectSignal;
2079
+ }
2063
2080
  const aIsArray = Array.isArray(a);
2064
2081
  const bIsArray = Array.isArray(b);
2065
2082
  if (aIsArray !== bIsArray) {
@@ -2540,8 +2557,6 @@ const setActionPrivateProperties = (action, properties) => {
2540
2557
  actionPrivatePropertiesWeakMap.set(action, properties);
2541
2558
  };
2542
2559
 
2543
- const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
2544
-
2545
2560
  /*
2546
2561
  * Actions: async callbacks wrapped in reactive state.
2547
2562
  *
@@ -4122,22 +4137,16 @@ const createActionProxyFromSignal = (
4122
4137
  dataSignal: proxySignal("dataSignal", "data"),
4123
4138
  });
4124
4139
  Object.preventExtensions(actionProxy);
4125
- // Watch for changes in the original paramsSignal and update ours
4126
- // (original signal wins over any replaceParams calls)
4127
- weakEffect(
4128
- [paramsSignal, proxyParamsSignal],
4129
- (paramsSignalRef, proxyParamsSignalRef) => {
4130
- const newParams = paramsSignalRef.value;
4131
- proxyParamsSignalRef.value = newParams;
4132
- },
4133
- );
4134
- weakEffect([action], () => {
4135
- // eslint-disable-next-line no-unused-expressions
4136
- proxyParamsSignal.value;
4137
- _updateTarget({
4138
- changeCause: "params_signal_change",
4139
- });
4140
- });
4140
+
4141
+ // What the proxy shows of the action it stands for is wired BEFORE the
4142
+ // effects below, because those resolve the first target as they are created:
4143
+ // a subscriber registered after them misses that first change and reads the
4144
+ // placeholders assigned above until a SECOND one happens. A proxy answering
4145
+ // `params: undefined` is then taken for a proxy holding no params, and
4146
+ // whoever asks it to run on truthy params does nothing (actionRunEffect's
4147
+ // onChange). The caller's own callbacks (onChange, runOnce, rerunOnChange)
4148
+ // stay below on purpose: they are told about changes, not about the proxy
4149
+ // coming into existence.
4141
4150
  onActionTargetChange((actionTarget) => {
4142
4151
  const currentAction = actionTarget || action;
4143
4152
  nameSignal.value = `[Proxy] ${currentAction.name}`;
@@ -4172,7 +4181,22 @@ const createActionProxyFromSignal = (
4172
4181
  });
4173
4182
  setActionPrivateProperties(actionProxy, proxyPrivateProperties);
4174
4183
  }
4175
-
4184
+ // Watch for changes in the original paramsSignal and update ours
4185
+ // (original signal wins over any replaceParams calls)
4186
+ weakEffect(
4187
+ [paramsSignal, proxyParamsSignal],
4188
+ (paramsSignalRef, proxyParamsSignalRef) => {
4189
+ const newParams = paramsSignalRef.value;
4190
+ proxyParamsSignalRef.value = newParams;
4191
+ },
4192
+ );
4193
+ weakEffect([action], () => {
4194
+ // eslint-disable-next-line no-unused-expressions
4195
+ proxyParamsSignal.value;
4196
+ _updateTarget({
4197
+ changeCause: "params_signal_change",
4198
+ });
4199
+ });
4176
4200
  actionProxy.replaceParams = (newParams) => {
4177
4201
  if (currentAction === action) {
4178
4202
  const currentParams = proxyParamsSignal.value;
@@ -22474,10 +22498,11 @@ let baseUrl;
22474
22498
  * "/places/:placeId". The url "/admin/places/42" matches, and `buildUrl` puts
22475
22499
  * the prefix back.
22476
22500
  *
22477
- * Call it in the routes module, above the `route()` calls it governs — they
22478
- * read the base url as they are created, so an entry point calling it after
22479
- * importing them is too late. The server must serve the document for every
22480
- * address below it (see docs/navigation.md).
22501
+ * Call it in the routes module, above `setupRoutes()`: the base url is read
22502
+ * whenever an address is matched or built, and the first match happens as the
22503
+ * routes are set up — an entry point calling it after importing them is too
22504
+ * late. The server must serve the document for every address below it (see
22505
+ * docs/navigation.md).
22481
22506
  *
22482
22507
  * @param {string} value - the document's own url, absolute or relative.
22483
22508
  */
@@ -47729,6 +47754,16 @@ const ButtonConfirm = ({
47729
47754
  restoreFocusRef.current = restoreFocus;
47730
47755
  setAsking(false);
47731
47756
  };
47757
+ const {
47758
+ action,
47759
+ command,
47760
+ onActionEnd,
47761
+ onClick
47762
+ } = props;
47763
+ // A submit button does not run anything itself: the form around it does,
47764
+ // with the button as requester, and the outcome never comes back through the
47765
+ // button's own action events. The question then waits on the form.
47766
+ const waitsForForm = !action && command === "--navi-send";
47732
47767
  useLayoutEffect(() => {
47733
47768
  if (asking) {
47734
47769
  // The press was aimed at this button, and the button that answers now is
@@ -47741,7 +47776,39 @@ const ButtonConfirm = ({
47741
47776
  if (found) {
47742
47777
  moveFocusTo(found.target);
47743
47778
  }
47744
- return;
47779
+ if (!waitsForForm) {
47780
+ return undefined;
47781
+ }
47782
+ const buttonEl = props.ref.current;
47783
+ const formEl = buttonEl.form || buttonEl.closest("form");
47784
+ if (!formEl) {
47785
+ return undefined;
47786
+ }
47787
+ // Same reading as watchActionCompletion (control_action.js): a send that
47788
+ // failed leaves its message on the button that asked, so the question
47789
+ // stays up with it; one that went through is answered.
47790
+ const onFormActionStart = actionStartEvent => {
47791
+ const {
47792
+ requester,
47793
+ addSideEffect
47794
+ } = actionStartEvent.detail;
47795
+ if (requester && requester !== buttonEl) {
47796
+ return;
47797
+ }
47798
+ addSideEffect(({
47799
+ error,
47800
+ aborted
47801
+ }) => {
47802
+ if (error || aborted) {
47803
+ return;
47804
+ }
47805
+ cancel(true);
47806
+ });
47807
+ };
47808
+ formEl.addEventListener("navi_action_start", onFormActionStart);
47809
+ return () => {
47810
+ formEl.removeEventListener("navi_action_start", onFormActionStart);
47811
+ };
47745
47812
  }
47746
47813
  if (restoreFocusRef.current) {
47747
47814
  restoreFocusRef.current = false;
@@ -47753,28 +47820,22 @@ const ButtonConfirm = ({
47753
47820
  moveFocusTo(buttonEl);
47754
47821
  }
47755
47822
  }
47823
+ return undefined;
47756
47824
  }, [asking]);
47757
47825
  if (!asking) {
47758
47826
  return jsx(Next, {
47759
47827
  ...props,
47760
- // The first press is the question, not the act — so nothing that acts
47761
- // is handed to the resolvers below. `type` with them: a submit button
47762
- // would be given `--navi-send` back.
47763
- type: "button",
47828
+ // The first press is the question, not the act: nothing that acts
47829
+ // reaches the ui. What was decided about the button itself (readOnly,
47830
+ // cta, the label) stays.
47764
47831
  action: undefined,
47765
47832
  command: undefined,
47766
47833
  href: undefined,
47767
- route: undefined,
47768
47834
  onClick: () => {
47769
47835
  setAsking(true);
47770
47836
  }
47771
47837
  });
47772
47838
  }
47773
- const {
47774
- action,
47775
- onActionEnd,
47776
- onClick
47777
- } = props;
47778
47839
  return jsxs("span", {
47779
47840
  className: "navi_button_confirm",
47780
47841
  ref: askingRef,
@@ -47800,7 +47861,7 @@ const ButtonConfirm = ({
47800
47861
  "data-testid": confirmTestId || props["data-testid"],
47801
47862
  onClick: e => {
47802
47863
  onClick?.(e);
47803
- if (!action) {
47864
+ if (!action && !waitsForForm) {
47804
47865
  // A command runs on the press itself: there is nothing to wait for,
47805
47866
  // the question is answered as soon as it is pressed.
47806
47867
  cancel(true);
@@ -48032,7 +48093,10 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48032
48093
  * button with this question and a "Confirmer"/"Annuler" pair, the second one
48033
48094
  * does what the button was for. `true` asks navi's default question. The
48034
48095
  * press stays under the finger and there is nothing to dismiss — "Annuler",
48035
- * Escape, or the focus leaving puts the button back. Use `<Picker
48096
+ * Escape, or the focus leaving puts the button back. So does the act itself,
48097
+ * once it went through: an `action` that succeeded, a submit whose form has
48098
+ * sent, a command on the press that runs it — while a failure keeps the
48099
+ * question up, its message on the button that asked. Use `<Picker
48036
48100
  * type="confirm">` instead when the question is long enough to want a popup,
48037
48101
  * or when the row has no space for it.
48038
48102
  * @param {import("ignore:preact").ComponentChildren} [confirmLabel] Label of the
@@ -48055,7 +48119,7 @@ const COMMAND_DEFAULT_PROPS_FACTORIES = {
48055
48119
  * (it wears it itself) and loses the shrink under the finger, which has
48056
48120
  * nothing left to scale but the interactive area itself.
48057
48121
  */
48058
- const Button = createComponentResolver([ButtonFirstResolver, ButtonConfirmResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonUI]);
48122
+ const Button = createComponentResolver([ButtonFirstResolver, ButtonRouteResolver, ButtonCommandPropResolver, ButtonConfirmResolver, ButtonUI]);
48059
48123
 
48060
48124
  installImportMetaCssBuild(import.meta);
48061
48125
  const css$O = /* css */`.navi_control_swap {
@@ -81205,6 +81269,7 @@ const css$9 = /* css */`@layer navi;
81205
81269
  * unitSize?: string,
81206
81270
  * unitSizeRatio?: number,
81207
81271
  * unitColor?: string,
81272
+ * unitSpacing?: string | number,
81208
81273
  * label?: string,
81209
81274
  * size?: string,
81210
81275
  * lang?: string,
@@ -81215,6 +81280,13 @@ const css$9 = /* css */`@layer navi;
81215
81280
  * bold?: boolean,
81216
81281
  * [key: string]: any,
81217
81282
  * }>}
81283
+ *
81284
+ * @param {string|number} [unitSpacing]
81285
+ * Gap between the value and the unit when `unitPosition="right"`. Accepts a
81286
+ * size token (`"s"`, `"m"`, …), a CSS length string, a number (px) or
81287
+ * `"0"` to glue the unit to the value. Defaults to a regular space
81288
+ * character. Ignored when `unitPosition="bottom"`, where the unit sits on
81289
+ * its own line.
81218
81290
  */
81219
81291
  const Quantity = ({
81220
81292
  children,
@@ -81223,6 +81295,7 @@ const Quantity = ({
81223
81295
  unitSize = "smaller",
81224
81296
  unitSizeRatio,
81225
81297
  unitColor,
81298
+ unitSpacing,
81226
81299
  label,
81227
81300
  size,
81228
81301
  lang,
@@ -81261,7 +81334,7 @@ const Quantity = ({
81261
81334
  }), jsxs(Text, {
81262
81335
  className: "navi_quantity_body",
81263
81336
  size: size,
81264
- spacing: unitBottom ? jsx("br", {}) : undefined,
81337
+ spacing: unitBottom ? jsx("br", {}) : unitSpacing,
81265
81338
  children: [jsx("span", {
81266
81339
  className: "navi_quantity_value",
81267
81340
  children: loading ? jsx(Icon, {