@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" onNavigate={() => {}}
39
- appSwitcher={appSwitcher ? { ...ACCOUNTING_APP_SWITCHER, activeId: app, onSelect: setApp } : null}>
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 `app-switcher` is on and no
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", label: "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
- items={navItems}
244
- sections={navSections}
245
- activeId={activeNavId}
246
- onNavigate={onNavigate}
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloimjolopez-pathway/pathway-components",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Pathway Design System React components",
5
5
  "type": "module",
6
6
  "sideEffects": false,