@the-portland-company/shell 0.99.1 → 0.100.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,3 +1,12 @@
1
+ ## 0.100.0
2
+
3
+ New pages open at the top (Ben 2026-11-08, clicked "Save as written" and "Go, all Modes" in the scroll-to-top session box; Reference/UI-Platform → URL state → "Scroll position"). Ben: "when clicking from one tab to another … the next page starts out slightly scrolled down. I need every new page to start fully scrolled at the top."
4
+
5
+ - **Cause.** Next's App Router only scrolls when the new page's top edge is off screen, so a page scrolled down a little kept that offset. And most pages and tabs switch by query parameter (`?page=…&tab=…`) on the same path, which nothing reset.
6
+ - **Fix.** `NavAppShell` (so every app on `PolitogyAppFrame`) now scrolls the window to the top, instantly, when the URL moves to a new page: a different path, or a different `mode`, `page` or `tab`. Changing only `section`, `org_id` or a feature parameter (filters, pagination) keeps the place; filling a missing default is not a new page; Back / Forward keep the browser's own position; a `#anchor` URL lands on its anchor. Uses the Navigation API where present, with a `popstate` + render-time check as the fallback, and survives apps that remount their chrome on each page (Tribes, Surveys).
7
+ - New exports on `./native`: `useScrollToTopOnNewPage`, `isNewPage`, `pageIdentityOf`, type `PageIdentity`.
8
+ - Tests: `__tests__/scrollToTop.test.tsx` (new).
9
+
1
10
  ## 0.99.1
2
11
 
3
12
  The user card says "Account Manager" for an Account Manager (bug seen live 2026-11-07: benedtl@hotmail.com, `account_manager` in "TEST Campaign A (link test)", a Linked Accounts roster membership, was shown as "Member").
@@ -422,6 +422,75 @@ function SpeculationRulesScript({ items }) {
422
422
  }, [items, origin]);
423
423
  return null;
424
424
  }
425
+ var PAGE_KEYS = ["mode", "page", "tab"];
426
+ function pageIdentityOf(pathname, search) {
427
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
428
+ const path = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
429
+ return {
430
+ pathname: path || "/",
431
+ mode: params.get("mode") || null,
432
+ page: params.get("page") || null,
433
+ tab: params.get("tab") || null
434
+ };
435
+ }
436
+ function isNewPage(prev, next) {
437
+ if (prev.pathname !== next.pathname) return true;
438
+ return PAGE_KEYS.some((k) => prev[k] !== null && next[k] !== prev[k]);
439
+ }
440
+ function scrollWindowToTop() {
441
+ if (window.scrollX === 0 && window.scrollY === 0) return;
442
+ try {
443
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
444
+ } catch {
445
+ window.scrollTo(0, 0);
446
+ }
447
+ }
448
+ var lastIdentity = null;
449
+ var listeners = 0;
450
+ var detach = null;
451
+ function currentIdentity() {
452
+ return pageIdentityOf(window.location.pathname, window.location.search);
453
+ }
454
+ function check(traverse = false) {
455
+ const next = currentIdentity();
456
+ const prev = lastIdentity;
457
+ lastIdentity = next;
458
+ if (!prev || traverse) return;
459
+ if (!isNewPage(prev, next)) return;
460
+ if (window.location.hash) return;
461
+ scrollWindowToTop();
462
+ }
463
+ function attach() {
464
+ const nav = window.navigation;
465
+ if (nav && typeof nav.addEventListener === "function") {
466
+ const onEntryChange = (e) => {
467
+ check(e.navigationType === "traverse");
468
+ };
469
+ nav.addEventListener("currententrychange", onEntryChange);
470
+ return () => nav.removeEventListener("currententrychange", onEntryChange);
471
+ }
472
+ const onPopState = () => check(true);
473
+ window.addEventListener("popstate", onPopState);
474
+ return () => window.removeEventListener("popstate", onPopState);
475
+ }
476
+ var useIsoLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
477
+ function useScrollToTopOnNewPage() {
478
+ react.useEffect(() => {
479
+ if (listeners === 0) detach = attach();
480
+ listeners += 1;
481
+ return () => {
482
+ listeners -= 1;
483
+ if (listeners === 0) {
484
+ detach?.();
485
+ detach = null;
486
+ }
487
+ };
488
+ }, []);
489
+ useIsoLayoutEffect(() => {
490
+ if (lastIdentity) check();
491
+ else lastIdentity = currentIdentity();
492
+ });
493
+ }
425
494
 
426
495
  // src/native/credits.ts
427
496
  var CREDIT_TYPE_ORDER = [
@@ -3463,6 +3532,7 @@ function NavAppShell({
3463
3532
  children
3464
3533
  }) {
3465
3534
  useCloseNavDetailsOnOutsideClick();
3535
+ useScrollToTopOnNewPage();
3466
3536
  if (contentWrapper === "bare") {
3467
3537
  warnDeprecated(
3468
3538
  "contentWrapper",
@@ -3673,7 +3743,7 @@ function watchLogout(onLogout, options = {}) {
3673
3743
  cleanup();
3674
3744
  onLogout();
3675
3745
  };
3676
- const check = () => {
3746
+ const check2 = () => {
3677
3747
  const at = readLogoutCookie();
3678
3748
  if (at !== null) fire(at);
3679
3749
  };
@@ -3687,15 +3757,15 @@ function watchLogout(onLogout, options = {}) {
3687
3757
  } catch {
3688
3758
  channel = null;
3689
3759
  }
3690
- const timer = window.setInterval(check, intervalMs);
3691
- window.addEventListener("visibilitychange", check);
3692
- window.addEventListener("focus", check);
3693
- window.addEventListener("pageshow", check);
3760
+ const timer = window.setInterval(check2, intervalMs);
3761
+ window.addEventListener("visibilitychange", check2);
3762
+ window.addEventListener("focus", check2);
3763
+ window.addEventListener("pageshow", check2);
3694
3764
  function cleanup() {
3695
3765
  window.clearInterval(timer);
3696
- window.removeEventListener("visibilitychange", check);
3697
- window.removeEventListener("focus", check);
3698
- window.removeEventListener("pageshow", check);
3766
+ window.removeEventListener("visibilitychange", check2);
3767
+ window.removeEventListener("focus", check2);
3768
+ window.removeEventListener("pageshow", check2);
3699
3769
  try {
3700
3770
  channel?.close();
3701
3771
  } catch {