@helloimjolopez-pathway/pathway-components 0.3.1 → 0.3.2
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.
|
@@ -28,15 +28,16 @@ function App() {
|
|
|
28
28
|
// The app switcher's on/off, the same switch as the web component's
|
|
29
29
|
// `app-switcher` attribute. It lives in the page so it works in every width.
|
|
30
30
|
const [appSwitcher, setAppSwitcher] = useState(false);
|
|
31
|
-
const [app, setApp] = useState("financials-home");
|
|
32
31
|
return (
|
|
33
32
|
<DemoChrome title="NavShell" widths={WIDTHS}
|
|
34
33
|
blurb={"TopNav, SideNav and the page area composed into one application frame. " +
|
|
35
34
|
"Desktop pushes the page with a collapsible nav, Tablet locks the 72px rail, " +
|
|
36
35
|
"Mobile opens the nav as an overlay from the hamburger. Tick App switcher to turn on the Accounting app switcher."}>
|
|
37
36
|
<NavShell activeModuleId="accounting" org={ORG} user={USER}
|
|
38
|
-
navSections={NAV_SECTIONS} activeNavId="balance_sheet"
|
|
39
|
-
|
|
37
|
+
navSections={NAV_SECTIONS} activeNavId="balance_sheet"
|
|
38
|
+
// NavShell tracks the current page and app itself. With the switcher on,
|
|
39
|
+
// each Accounting app brings its own side nav.
|
|
40
|
+
appSwitcher={appSwitcher ? ACCOUNTING_APP_SWITCHER : null}>
|
|
40
41
|
<PageTemplate>
|
|
41
42
|
<label style={{ display: "flex", alignItems: "center", gap: "var(--semantic-layout-units-gap-tight)",
|
|
42
43
|
fontSize: Y("font-size-s"), lineHeight: Y("line-height-s-single"),
|
|
@@ -75,17 +75,31 @@ function useBreakpoint() {
|
|
|
75
75
|
* defaultCollapsed boolean — initial desktop collapse state (default false)
|
|
76
76
|
* children ReactNode — page content (renders inside Shell.Main)
|
|
77
77
|
*/
|
|
78
|
-
// The Accounting module's apps, as Figma's Accounting.AppSwitcher lists them
|
|
79
|
-
// The web component falls back to this when
|
|
80
|
-
// `appSwitcher` property has been set.
|
|
78
|
+
// The Accounting module's apps, as Figma's Accounting.AppSwitcher lists them,
|
|
79
|
+
// each with its own side nav. The web component falls back to this when
|
|
80
|
+
// `app-switcher` is on and no `appSwitcher` property has been set.
|
|
81
|
+
//
|
|
82
|
+
// Financials Home's nav is Figma's SideNav.Local.Accounting (40016737:141666),
|
|
83
|
+
// icons included. Figma draws no other app's nav, so Accounts Payable,
|
|
84
|
+
// Accounts Receivable and Bank Reconciliation use the item lists the widget
|
|
85
|
+
// demo read from production (Bank Reconciliation takes Bank Account
|
|
86
|
+
// Management's), on the same Figma icons.
|
|
87
|
+
const accountingNav = (app, labels) => labels.map((l) => ({ id: `${app}-${l.toLowerCase().replace(/\s+/g, "-")}`, label: l, icon: ACCOUNTING_ICONS[l] }));
|
|
88
|
+
const ACCOUNTING_ICONS = {
|
|
89
|
+
Dashboard: "space_dashboard", Enter: "input", Manage: "bookmark_manager", View: "visibility",
|
|
90
|
+
Reports: "insert_chart", Modify: "edit_square", Help: "help", Utilities: "tools_wrench",
|
|
91
|
+
"Security Setup": "security",
|
|
92
|
+
};
|
|
93
|
+
const APP_NAV_STANDARD = ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Utilities", "Help"];
|
|
81
94
|
export const ACCOUNTING_APP_SWITCHER = {
|
|
82
95
|
label: "Financials Home",
|
|
83
96
|
icon: "apps",
|
|
84
|
-
home: { id: "financials-home", label: "Financials Home"
|
|
97
|
+
home: { id: "financials-home", label: "Financials Home",
|
|
98
|
+
navItems: accountingNav("fh", ["Dashboard", "Enter", "Manage", "View", "Reports", "Modify", "Help", "Utilities", "Security Setup"]) },
|
|
85
99
|
apps: [
|
|
86
|
-
{ id: "accounts-payable",
|
|
87
|
-
{ id: "accounts-receivable", label: "Accounts Receivable" },
|
|
88
|
-
{ id: "bank-reconciliation", label: "Bank Reconciliation" },
|
|
100
|
+
{ id: "accounts-payable", label: "Accounts Payable", navItems: accountingNav("ap", APP_NAV_STANDARD) },
|
|
101
|
+
{ id: "accounts-receivable", label: "Accounts Receivable", navItems: accountingNav("ar", APP_NAV_STANDARD) },
|
|
102
|
+
{ id: "bank-reconciliation", label: "Bank Reconciliation", navItems: accountingNav("br", APP_NAV_STANDARD) },
|
|
89
103
|
],
|
|
90
104
|
};
|
|
91
105
|
|
|
@@ -132,6 +146,49 @@ export function NavShell({
|
|
|
132
146
|
// opened with a 72 rail inside it and 178px of blank.
|
|
133
147
|
const effectiveCollapsed = isTablet ? true : isMobile ? false : collapsed;
|
|
134
148
|
|
|
149
|
+
// ── Current page and current app ──────────────────────────────────────────
|
|
150
|
+
// Both follow their props when given, and otherwise NavShell tracks them
|
|
151
|
+
// itself, so a tap always moves the selection even when the host only
|
|
152
|
+
// listens to onNavigate (or does not listen at all). Before this the shell
|
|
153
|
+
// was purely controlled: a host that did not echo activeNavId back got a nav
|
|
154
|
+
// whose items looked dead.
|
|
155
|
+
const [navActive, setNavActive] = useState(activeNavId);
|
|
156
|
+
useEffect(() => { setNavActive(activeNavId); }, [activeNavId]);
|
|
157
|
+
|
|
158
|
+
const apps = appSwitcher ? [appSwitcher.home, ...(appSwitcher.apps || [])].filter(Boolean) : [];
|
|
159
|
+
const [appActive, setAppActive] = useState(appSwitcher && (appSwitcher.activeId ?? (appSwitcher.home && appSwitcher.home.id)));
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (appSwitcher && appSwitcher.activeId !== undefined) setAppActive(appSwitcher.activeId);
|
|
162
|
+
}, [appSwitcher && appSwitcher.activeId]);
|
|
163
|
+
const currentApp = apps.find((a) => a.id === appActive);
|
|
164
|
+
|
|
165
|
+
// An app that carries its own nav replaces the shell's: choosing Accounts
|
|
166
|
+
// Payable shows Accounts Payable's items. Apps without one keep the shell's.
|
|
167
|
+
const appNav = currentApp && (currentApp.navItems || currentApp.navSections);
|
|
168
|
+
const sideItems = appNav ? currentApp.navItems : navItems;
|
|
169
|
+
const sideSections = appNav ? currentApp.navSections : navSections;
|
|
170
|
+
// Figma shows the app's Dashboard active on arrival, so when the current page
|
|
171
|
+
// is not one of this app's items, its first item is the active one.
|
|
172
|
+
const appIds = appNav
|
|
173
|
+
? (currentApp.navItems || currentApp.navSections.flatMap((x) => x.items))
|
|
174
|
+
.flatMap((i) => [i.id, ...((i.children || []).map((c) => c.id))])
|
|
175
|
+
: null;
|
|
176
|
+
const sideActive = appIds && !appIds.includes(navActive) ? appIds[0] : navActive;
|
|
177
|
+
|
|
178
|
+
const handleNavigate = useCallback((id) => {
|
|
179
|
+
setNavActive(id);
|
|
180
|
+
onNavigate && onNavigate(id);
|
|
181
|
+
}, [onNavigate]);
|
|
182
|
+
|
|
183
|
+
const handleAppSelect = useCallback((id) => {
|
|
184
|
+
setAppActive(id);
|
|
185
|
+
appSwitcher && appSwitcher.onSelect && appSwitcher.onSelect(id);
|
|
186
|
+
// Land on the new app's first page (its Dashboard).
|
|
187
|
+
const next = apps.find((a) => a.id === id);
|
|
188
|
+
const first = next && (next.navItems ? next.navItems[0] : next.navSections && next.navSections[0] && next.navSections[0].items[0]);
|
|
189
|
+
if (first) handleNavigate(first.id);
|
|
190
|
+
}, [appSwitcher, apps, handleNavigate]);
|
|
191
|
+
|
|
135
192
|
// Shell.Main margin-left: matches current SideNav width for push layout.
|
|
136
193
|
// Mobile: no margin (SideNav is an overlay).
|
|
137
194
|
const mainMarginLeft = isMobile
|
|
@@ -240,17 +297,19 @@ export function NavShell({
|
|
|
240
297
|
}}
|
|
241
298
|
>
|
|
242
299
|
<SideNav
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
300
|
+
// Remount per app so one app's open groups do not carry into the next.
|
|
301
|
+
key={appNav ? currentApp.id : "shell"}
|
|
302
|
+
items={sideItems}
|
|
303
|
+
sections={sideSections}
|
|
304
|
+
activeId={sideActive}
|
|
305
|
+
onNavigate={handleNavigate}
|
|
247
306
|
collapsed={effectiveCollapsed}
|
|
248
307
|
onCollapseChange={handleCollapseChange}
|
|
249
308
|
// Mobile: the TopNav hamburger is the only toggle (NavHeader spec).
|
|
250
309
|
hideCollapseButton={isTablet || isMobile}
|
|
251
310
|
headerEnd={navHeaderEnd}
|
|
252
311
|
footer={navFooter}
|
|
253
|
-
appSwitcher={appSwitcher}
|
|
312
|
+
appSwitcher={appSwitcher ? { ...appSwitcher, activeId: appActive, onSelect: handleAppSelect } : null}
|
|
254
313
|
>
|
|
255
314
|
{navChildren}
|
|
256
315
|
</SideNav>
|
|
@@ -235,9 +235,12 @@ export function SideNavItem({
|
|
|
235
235
|
: (isActive || isCollapsedActiveGrouper || isExpandedGrouper) ? T.text.navActive
|
|
236
236
|
: hovered ? T.text.navHover
|
|
237
237
|
: T.text.navBase;
|
|
238
|
+
// Figma SideNavItem State=Trail (an open grouper) binds Secondary/Pressed on
|
|
239
|
+
// the leading icon AND the chevron, as it does on the label. The icon used to
|
|
240
|
+
// stay on Rest/Hover there, so an open group's glyphs read lighter than its label.
|
|
238
241
|
const iconColor =
|
|
239
242
|
isDisabled ? T.icon.navDisabled
|
|
240
|
-
: (isActive || isCollapsedActiveGrouper) ? T.icon.navActive
|
|
243
|
+
: (isActive || isCollapsedActiveGrouper || isExpandedGrouper) ? T.icon.navActive
|
|
241
244
|
: hovered ? T.icon.navHover
|
|
242
245
|
: T.icon.navBase;
|
|
243
246
|
|
|
@@ -581,7 +584,12 @@ const APP_T = {
|
|
|
581
584
|
separator: c("stroke-static-neutral-base"),
|
|
582
585
|
};
|
|
583
586
|
|
|
584
|
-
export function AppSwitcher({ label, icon = "apps", home, apps = [], activeId, onSelect, collapsed = false }) {
|
|
587
|
+
export function AppSwitcher({ label: fallbackLabel, icon = "apps", home, apps = [], activeId, onSelect, collapsed = false }) {
|
|
588
|
+
// The trigger names WHERE YOU ARE: the current app's label, so after choosing
|
|
589
|
+
// Accounts Payable it reads Accounts Payable. `label` is only the fallback
|
|
590
|
+
// for when no app is current.
|
|
591
|
+
const current = [home, ...apps].filter(Boolean).find(a => a.id === activeId);
|
|
592
|
+
const label = (current && current.label) || fallbackLabel || (home && home.label);
|
|
585
593
|
const [hov, setHov] = useState(false);
|
|
586
594
|
const [press, setPress] = useState(false);
|
|
587
595
|
const [open, setOpen] = useState(false);
|
|
@@ -614,7 +622,7 @@ export function AppSwitcher({ label, icon = "apps", home, apps = [], activeId, o
|
|
|
614
622
|
const active = open || press;
|
|
615
623
|
const fg = active ? APP_T.openFg : hov ? APP_T.hoverFg : APP_T.restFg;
|
|
616
624
|
const chevron = active ? APP_T.openFg : hov ? APP_T.hoverFg : APP_T.restChevron;
|
|
617
|
-
const homeItem = home || { id: "__home", label };
|
|
625
|
+
const homeItem = home || { id: "__home", label: fallbackLabel };
|
|
618
626
|
|
|
619
627
|
return (
|
|
620
628
|
// Full column width in both states: 218 expanded, the rail's 48 collapsed,
|