@dloizides/ui-nav 1.19.0 → 1.21.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.20.0
4
+
5
+ **The auth-pending and page-loading spinners now OVERLAY stably-mounted children instead of
6
+ replacing them — closing a latent deep-link-collapse trap in the shared shell.**
7
+
8
+ `AppShell`'s `gate.pending` branch and `appShellContent`'s `state.loading` branch each `return`ed a
9
+ full-screen spinner *instead of* `children`. Replacing the subtree unmounts whatever `children` host
10
+ — in a portal that is a navigator/`<Slot/>` — and on remount React Navigation re-derives its default
11
+ route, so a deep-linked sub-route silently collapses to its default on every reload. (This is the
12
+ exact class of bug that hit kefi-web's organizer portal, fixed there in its `OnboardingGate`; the
13
+ same shape lived dormant here because no consumer passes `gate=`/`state=` yet.)
14
+
15
+ - **`AppShell` (`gate.pending`)** now renders the shell tree normally and paints the pending spinner
16
+ as an opaque `position:absolute` overlay on top, so `children` mount once and are never torn down
17
+ across `pending → authenticated` (proven by a "same child instance, no remount" test).
18
+ - **`appShellContent` (`state.loading`)** likewise overlays the loading spinner over mounted
19
+ `children` rather than swapping them out. The `loadingHost` wrapper is gated on `state` being
20
+ DEFINED, not on `loading`'s live value — so the element type at the mount point is invariant across
21
+ a `loading` true↔false toggle and `children` are reconciled in place, never remounted on the edge
22
+ (also proven by a "same child instance, no remount" test). Consumers that pass no `state` render
23
+ `children` bare, byte-identical to 1.19.0. Error / forbidden cards are TERMINAL and still replace;
24
+ unauthenticated still returns null + `onRedirect`.
25
+ - **Rule encoded in the docblock:** a *transitional* status overlays; only a *terminal* state
26
+ replaces. Corollary for consumers — `children` mount beneath the pending overlay, so gate data
27
+ FETCHES on auth state (`enabled: isAuthenticated`), not on mount. No public API changed; behavior
28
+ is unchanged for any consumer not passing `gate`/`state`.
29
+
3
30
  ## 1.19.0
4
31
 
5
32
  **The "More ▾" overflow menu now opens at a readable width instead of cramping onto the trigger's tiny footprint.**
package/dist/index.d.mts CHANGED
@@ -595,6 +595,16 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
595
595
  * (pending spinner / redirect-when-unauthenticated) and page state cards
596
596
  * (loading / error / forbidden).
597
597
  *
598
+ * OVERLAY DISCIPLINE (do not regress): a TRANSITIONAL status — `gate.pending` and
599
+ * `state.loading` — renders as an absolute overlay OVER stably-mounted `children`,
600
+ * never as a replacement. Replacing the subtree unmounts whatever `children` host
601
+ * (typically a navigator/`<Slot/>`); on remount React Navigation re-derives its
602
+ * default route, so a deep-linked sub-route silently collapses to its default on
603
+ * every reload. Only TERMINAL states replace: unauthenticated returns null (we are
604
+ * navigating away) and error/forbidden cards win inside the content column.
605
+ * Corollary for consumers: `children` mount BENEATH the pending overlay, so gate
606
+ * your data FETCHES on auth state (`enabled: isAuthenticated`), not on mount.
607
+ *
598
608
  * A BACK-OFFICE variant is supported via the optional `sidebar` slot (added for
599
609
  * Agora's merchant admin): supply a `<Sidebar>` and the body below the header /
600
610
  * nav / banner becomes a ROW — a persistent left rail beside the scrolling
package/dist/index.d.ts CHANGED
@@ -595,6 +595,16 @@ declare function useContentMaxWidth(width: AppShellWidth): number | 'full';
595
595
  * (pending spinner / redirect-when-unauthenticated) and page state cards
596
596
  * (loading / error / forbidden).
597
597
  *
598
+ * OVERLAY DISCIPLINE (do not regress): a TRANSITIONAL status — `gate.pending` and
599
+ * `state.loading` — renders as an absolute overlay OVER stably-mounted `children`,
600
+ * never as a replacement. Replacing the subtree unmounts whatever `children` host
601
+ * (typically a navigator/`<Slot/>`); on remount React Navigation re-derives its
602
+ * default route, so a deep-linked sub-route silently collapses to its default on
603
+ * every reload. Only TERMINAL states replace: unauthenticated returns null (we are
604
+ * navigating away) and error/forbidden cards win inside the content column.
605
+ * Corollary for consumers: `children` mount BENEATH the pending overlay, so gate
606
+ * your data FETCHES on auth state (`enabled: isAuthenticated`), not on mount.
607
+ *
598
608
  * A BACK-OFFICE variant is supported via the optional `sidebar` slot (added for
599
609
  * Agora's merchant admin): supply a `<Sidebar>` and the body below the header /
600
610
  * nav / banner becomes a ROW — a persistent left rail beside the scrolling
package/dist/index.js CHANGED
@@ -646,9 +646,7 @@ var NavExpandableItem = ({
646
646
  )) }) })
647
647
  ] });
648
648
  };
649
-
650
- // src/searchAffordance.ts
651
- var DEFAULT_SEARCH_BREAKPOINT = 768;
649
+ var DEFAULT_SEARCH_BREAKPOINT = uiLayout.LAYOUT_COLLAPSE_BREAKPOINT;
652
650
  function resolveSearchAffordance(viewport, breakpoint = DEFAULT_SEARCH_BREAKPOINT) {
653
651
  return viewport >= breakpoint ? "inline" : "palette";
654
652
  }
@@ -1344,7 +1342,22 @@ var CARD_TITLE_FONT_SIZE = 16;
1344
1342
  var CARD_MESSAGE_FONT_SIZE = 14;
1345
1343
  var CARD_TITLE_MARGIN_BOTTOM = 8;
1346
1344
  var styles3 = reactNative.StyleSheet.create({
1347
- centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
1345
+ // Loading is a TRANSITIONAL state: the spinner is an absolute overlay ON TOP of
1346
+ // stably-mounted children, never a replacement for them. Replacing children here
1347
+ // would unmount whatever they host (e.g. a navigator), and on remount React
1348
+ // Navigation re-derives its default route — silently discarding a deep link.
1349
+ // The host wraps children whenever a `state` is passed (not only while loading),
1350
+ // so its element type is invariant across the loading toggle — see `useContentBody`.
1351
+ loadingHost: { flex: 1 },
1352
+ loadingOverlay: {
1353
+ alignItems: "center",
1354
+ bottom: 0,
1355
+ justifyContent: "center",
1356
+ left: 0,
1357
+ position: "absolute",
1358
+ right: 0,
1359
+ top: 0
1360
+ },
1348
1361
  card: {
1349
1362
  padding: CARD_PADDING,
1350
1363
  borderRadius: CARD_BORDER_RADIUS,
@@ -1369,13 +1382,22 @@ function useContentBody(state, children, testID) {
1369
1382
  const { theme } = uiFeedback.useUi();
1370
1383
  const primary = theme.palette.primary["500"];
1371
1384
  const errorColor = theme.semantic.error["500"];
1372
- if (state?.forbidden)
1385
+ if (!state) return children;
1386
+ if (state.forbidden)
1373
1387
  return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.forbidden, testID: `${testID}${APP_SHELL_SUFFIX.forbidden}` });
1374
- if (state?.error)
1388
+ if (state.error)
1375
1389
  return /* @__PURE__ */ jsxRuntime.jsx(MessageCard, { accentColor: errorColor, message: state.error, testID: `${testID}${APP_SHELL_SUFFIX.error}` });
1376
- if (state?.loading === true)
1377
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles3.centerFill, testID: `${testID}${APP_SHELL_SUFFIX.loading}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
1378
- return children;
1390
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles3.loadingHost, children: [
1391
+ children,
1392
+ state.loading === true ? /* @__PURE__ */ jsxRuntime.jsx(
1393
+ reactNative.View,
1394
+ {
1395
+ style: [styles3.loadingOverlay, { backgroundColor: theme.colors.background }],
1396
+ testID: `${testID}${APP_SHELL_SUFFIX.loading}`,
1397
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" })
1398
+ }
1399
+ ) : null
1400
+ ] });
1379
1401
  }
1380
1402
  var SCRIM_COLOR = "rgba(0, 0, 0, 0.5)";
1381
1403
  var MENU_GLYPH2 = "\u2630";
@@ -1487,9 +1509,7 @@ var MobileDrawer = ({
1487
1509
  )
1488
1510
  ] });
1489
1511
  };
1490
-
1491
- // src/railMode.ts
1492
- var RAIL_FULL_BREAKPOINT = 768;
1512
+ var RAIL_FULL_BREAKPOINT = uiLayout.LAYOUT_COLLAPSE_BREAKPOINT;
1493
1513
  var DEFAULT_COLLAPSED_RAIL_MAX = 1024;
1494
1514
  function resolveRailMode({ viewport, hasSidebar, hasCollapsed, range }) {
1495
1515
  if (!hasSidebar) return "none";
@@ -1513,7 +1533,10 @@ function useContentMaxWidth(width) {
1513
1533
  var DEFAULT_CONTENT_PADDING = 24;
1514
1534
  var styles5 = reactNative.StyleSheet.create({
1515
1535
  root: { flex: 1 },
1516
- centerFill: { flex: 1, justifyContent: "center", alignItems: "center" },
1536
+ // Auth-pending spinner: an opaque absolute overlay over the mounted shell, so
1537
+ // `children` (and any navigator they host) are never unmounted while the gate
1538
+ // resolves. See the `gatePending` note in the render body.
1539
+ pendingOverlay: { alignItems: "center", bottom: 0, justifyContent: "center", left: 0, position: "absolute", right: 0, top: 0 },
1517
1540
  scroll: { flex: 1 },
1518
1541
  scrollContent: { flexGrow: 1 },
1519
1542
  // Mobile back-office header: the hamburger sits inline before the header slot.
@@ -1567,8 +1590,7 @@ var AppShell = ({
1567
1590
  React.useEffect(() => {
1568
1591
  if (isUnauthenticated && gate !== void 0) gate.onRedirect();
1569
1592
  }, [isUnauthenticated, gate]);
1570
- if (gate?.pending === true)
1571
- return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles5.root, styles5.centerFill, { backgroundColor: theme.colors.background }], testID: `${testID}${APP_SHELL_SUFFIX.pending}`, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" }) });
1593
+ const gatePending = gate?.pending === true;
1572
1594
  if (isUnauthenticated) return null;
1573
1595
  const columnStyle = maxWidth === "full" ? styles5.columnFull : [styles5.columnCapped, { maxWidth }];
1574
1596
  const navInnerStyle = chromeAlignment === "content" && maxWidth !== "full" ? [styles5.chromeCapped, { maxWidth, paddingHorizontal: contentPadding }] : void 0;
@@ -1611,6 +1633,14 @@ var AppShell = ({
1611
1633
  sidebar,
1612
1634
  onClose: closeDrawer
1613
1635
  }
1636
+ ) : null,
1637
+ gatePending ? /* @__PURE__ */ jsxRuntime.jsx(
1638
+ reactNative.View,
1639
+ {
1640
+ style: [styles5.pendingOverlay, { backgroundColor: theme.colors.background }],
1641
+ testID: `${testID}${APP_SHELL_SUFFIX.pending}`,
1642
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.ActivityIndicator, { color: primary, size: "large" })
1643
+ }
1614
1644
  ) : null
1615
1645
  ] });
1616
1646
  };