@the-portland-company/shell 0.99.0 → 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 +17 -0
- package/dist/app-chrome.cjs +117 -10
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +8 -1
- package/dist/app-chrome.d.ts +8 -1
- package/dist/app-chrome.js +41 -4
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-YMCWLO2L.js → chunk-2JH5AWUG.js} +74 -4
- package/dist/chunk-2JH5AWUG.js.map +1 -0
- package/dist/{index-Di9AyVsU.d.ts → index-2RNBy1XE.d.ts} +16 -1
- package/dist/{index-DhrhpBVm.d.cts → index-B57_u6pS.d.cts} +16 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/native.cjs +73 -0
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +1 -1
- package/dist/native.d.ts +1 -1
- package/dist/native.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-YMCWLO2L.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.99.1
|
|
11
|
+
|
|
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").
|
|
13
|
+
|
|
14
|
+
- **Role line from the real role.** The card under the name printed the app's own role string, and apps collapse roles to their permission pair (Contacts: admin | member), so `account_manager` came out as "Member". `PolitogyAppFrame` now labels the active organization's raw membership role (the chrome API's `rawRole`; in token mode the `org_role` claim) and falls back to the app's string. Labels (Specs/Organization-Settings §8.4 groups): owner / admin / account_admin → "Admin" and member / field roles → "Member" as before; account_manager → "Account Manager"; viewer → "Viewer".
|
|
15
|
+
- `useChromeState()` gains `activeRawRole`. In controlled mode (Contacts) it comes from the chrome API org list when the app's list has no `rawRole`.
|
|
16
|
+
- Tests: `__tests__/userCardRole.test.tsx` (new).
|
|
17
|
+
|
|
1
18
|
## 0.99.0
|
|
2
19
|
|
|
3
20
|
VRM Verified badge and the Contact Card slide-in (Ben 2026-11-06, typed in the UI debugging box; specs Contacts §4.2, §8.6A and VRM-Verified §2 updated with his go).
|
package/dist/app-chrome.cjs
CHANGED
|
@@ -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
|
|
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(
|
|
3691
|
-
window.addEventListener("visibilitychange",
|
|
3692
|
-
window.addEventListener("focus",
|
|
3693
|
-
window.addEventListener("pageshow",
|
|
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",
|
|
3697
|
-
window.removeEventListener("focus",
|
|
3698
|
-
window.removeEventListener("pageshow",
|
|
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 {
|
|
@@ -4094,6 +4164,34 @@ function LoadingScreen({
|
|
|
4094
4164
|
);
|
|
4095
4165
|
}
|
|
4096
4166
|
|
|
4167
|
+
// src/lib/roleLabel.ts
|
|
4168
|
+
var ADMIN_ROLES = /* @__PURE__ */ new Set(["owner", "admin", "account_admin"]);
|
|
4169
|
+
var STAFF_ROLES = /* @__PURE__ */ new Set(["super_admin", "super administrator", "politogy_admin"]);
|
|
4170
|
+
var MANAGER_ROLES = /* @__PURE__ */ new Set(["account_manager", "account manager", "manager", "campaign_manager"]);
|
|
4171
|
+
var FIELD_ROLES = /* @__PURE__ */ new Set([
|
|
4172
|
+
"member",
|
|
4173
|
+
"field_user",
|
|
4174
|
+
"field user",
|
|
4175
|
+
"field",
|
|
4176
|
+
"volunteer",
|
|
4177
|
+
"canvasser",
|
|
4178
|
+
"caller",
|
|
4179
|
+
"staff"
|
|
4180
|
+
]);
|
|
4181
|
+
var VIEWER_ROLES = /* @__PURE__ */ new Set(["viewer"]);
|
|
4182
|
+
function roleLabel(raw) {
|
|
4183
|
+
const role = (raw ?? "").trim().toLowerCase();
|
|
4184
|
+
if (!role) return null;
|
|
4185
|
+
if (ADMIN_ROLES.has(role) || STAFF_ROLES.has(role)) return "Admin";
|
|
4186
|
+
if (MANAGER_ROLES.has(role)) return "Account Manager";
|
|
4187
|
+
if (FIELD_ROLES.has(role)) return "Member";
|
|
4188
|
+
if (VIEWER_ROLES.has(role)) return "Viewer";
|
|
4189
|
+
return null;
|
|
4190
|
+
}
|
|
4191
|
+
function userCardRole(activeRawRole, appRole) {
|
|
4192
|
+
return roleLabel(activeRawRole) ?? roleLabel(appRole) ?? (appRole?.trim() || null);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4097
4195
|
// src/url-state.ts
|
|
4098
4196
|
var URL_STATE_MODES = ["relationship", "campaign", "petition"];
|
|
4099
4197
|
var URL_STATE_KEYS = ["mode", "page", "tab", "section", "org_id"];
|
|
@@ -4538,7 +4636,9 @@ function FrameBody({
|
|
|
4538
4636
|
// The chrome API's avatar wins; the app may supply a fallback chain
|
|
4539
4637
|
// (e.g. a stored memoji) for users with no uploaded photo.
|
|
4540
4638
|
avatarUrl: identity.avatarUrl ?? data?.fallbackAvatarUrl ?? null,
|
|
4541
|
-
role
|
|
4639
|
+
// The active org's RAW role wins over the app's collapsed one, so an
|
|
4640
|
+
// Account Manager is not shown as "Member" (Organization-Settings §8.4).
|
|
4641
|
+
role: userCardRole(chrome?.activeRawRole ?? activeCoreOrg?.rawRole, session.role)
|
|
4542
4642
|
},
|
|
4543
4643
|
userMenu,
|
|
4544
4644
|
onSignOut: handleSignOut,
|
|
@@ -4726,10 +4826,13 @@ function TokenChromeStateProvider({
|
|
|
4726
4826
|
orgs,
|
|
4727
4827
|
orgsLoading: !tokenOrg.orgs && orgsFetching && !orgsError && !fetchedOrgs,
|
|
4728
4828
|
orgPending: pendingOrg !== null,
|
|
4829
|
+
// The list's raw role, else the token's `org_role` claim — only while the
|
|
4830
|
+
// token is still for the active org (not mid-switch).
|
|
4831
|
+
activeRawRole: orgs.find((o) => o.id === orgId)?.rawRole ?? (orgId && tokenOrg.orgId === orgId ? tokenOrg.orgRole ?? null : null),
|
|
4729
4832
|
navDisplay,
|
|
4730
4833
|
setNavDisplay
|
|
4731
4834
|
}),
|
|
4732
|
-
[orgId, setOrgId, orgs, tokenOrg.orgs, orgsFetching, orgsError, fetchedOrgs, pendingOrg, navDisplay, setNavDisplay]
|
|
4835
|
+
[orgId, setOrgId, orgs, tokenOrg.orgs, tokenOrg.orgId, tokenOrg.orgRole, orgsFetching, orgsError, fetchedOrgs, pendingOrg, navDisplay, setNavDisplay]
|
|
4733
4836
|
);
|
|
4734
4837
|
return /* @__PURE__ */ jsxRuntime.jsx(ChromeStateContext.Provider, { value, children });
|
|
4735
4838
|
}
|
|
@@ -4883,6 +4986,9 @@ function ChromeStateProvider({
|
|
|
4883
4986
|
})),
|
|
4884
4987
|
orgsLoading: false,
|
|
4885
4988
|
orgPending: false,
|
|
4989
|
+
// The app's list collapses roles (Contacts: admin | member); the
|
|
4990
|
+
// chrome API's list carries the raw one.
|
|
4991
|
+
activeRawRole: controlledOrg.orgs?.find((o) => o.id === controlledOrg.orgId)?.rawRole ?? orgs?.find((o) => o.id === controlledOrg.orgId)?.rawRole ?? null,
|
|
4886
4992
|
navDisplay,
|
|
4887
4993
|
setNavDisplay
|
|
4888
4994
|
};
|
|
@@ -4903,6 +5009,7 @@ function ChromeStateProvider({
|
|
|
4903
5009
|
// falls back to the app label rather than shimmering forever.
|
|
4904
5010
|
orgsLoading: orgsFetching && !orgsError && !orgs,
|
|
4905
5011
|
orgPending: false,
|
|
5012
|
+
activeRawRole: orgs?.find((o) => o.id === orgId)?.rawRole ?? null,
|
|
4906
5013
|
navDisplay,
|
|
4907
5014
|
setNavDisplay
|
|
4908
5015
|
};
|