@helloimjolopez-pathway/pathway-components 0.3.0 → 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.
@@ -8,9 +8,9 @@
8
8
  * a 240px nav, no item radius, a 4px stripe rounded at 4, a 20px glyph, a 22px
9
9
  * module mark, raw rgba hover fills, and a 56px NavHeader Figma does not draw.
10
10
  */
11
- import React from "react";
11
+ import React, { useState } from "react";
12
12
  import { DemoChrome, mount } from "../_demo/harness.jsx";
13
- import { NavShell } from "./nav-shell.jsx";
13
+ import { NavShell, ACCOUNTING_APP_SWITCHER } from "./nav-shell.jsx";
14
14
  import { PageTemplate } from "../page-template/page-template.jsx";
15
15
  import { NAV_SECTIONS } from "../../src/stories/Library/SideNav/sidenavDemoData.jsx";
16
16
 
@@ -25,14 +25,27 @@ const ORG = { id: "gcc", name: "Grace Community Church", campus: "Knoxville", in
25
25
  const USER = { name: "Jo Lopez", initials: "JL", email: "jo@gracecommunity.org" };
26
26
 
27
27
  function App() {
28
+ // The app switcher's on/off, the same switch as the web component's
29
+ // `app-switcher` attribute. It lives in the page so it works in every width.
30
+ const [appSwitcher, setAppSwitcher] = useState(false);
28
31
  return (
29
32
  <DemoChrome title="NavShell" widths={WIDTHS}
30
33
  blurb={"TopNav, SideNav and the page area composed into one application frame. " +
31
34
  "Desktop pushes the page with a collapsible nav, Tablet locks the 72px rail, " +
32
- "Mobile opens the nav as an overlay from the hamburger."}>
35
+ "Mobile opens the nav as an overlay from the hamburger. Tick App switcher to turn on the Accounting app switcher."}>
33
36
  <NavShell activeModuleId="accounting" org={ORG} user={USER}
34
- navSections={NAV_SECTIONS} activeNavId="balance_sheet" onNavigate={() => {}}>
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}>
35
41
  <PageTemplate>
42
+ <label style={{ display: "flex", alignItems: "center", gap: "var(--semantic-layout-units-gap-tight)",
43
+ fontSize: Y("font-size-s"), lineHeight: Y("line-height-s-single"),
44
+ color: C("foreground-static-neutral-base"), cursor: "pointer" }}>
45
+ <input id="navshell-app-switcher" type="checkbox" checked={appSwitcher}
46
+ onChange={(e) => setAppSwitcher(e.target.checked)} />
47
+ App switcher (Accounting)
48
+ </label>
36
49
  <h1 style={{ margin: 0, fontSize: Y("font-size-xl"), lineHeight: Y("line-height-xl-single"),
37
50
  fontWeight: Y("weight-semibold"), color: C("foreground-static-neutral-bold") }}>
38
51
  Balance Sheet
@@ -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>
@@ -105,7 +105,9 @@ export const T = {
105
105
  navActive: c("foreground-action-secondary-pressed"),
106
106
  navDisabled: c("foreground-action-disabled"),
107
107
  secondary: c("foreground-static-neutral-subtle"), // PopoverMenu.SectionHeader
108
- secondarySubtle: c("foreground-static-neutral-faint"), // NavSectionLabel
108
+ // NavSectionLabel. Subtle, not Faint: Faint at 11px measured 4.23:1 on
109
+ // white in Amplify Light, under AA's 4.5. Figma SectionLabel rebound to match.
110
+ secondarySubtle: c("foreground-static-neutral-subtle"),
109
111
  strong: c("foreground-static-neutral-strong"), // PopoverMenu.Item, tooltip
110
112
  },
111
113
  icon: {
@@ -233,9 +235,12 @@ export function SideNavItem({
233
235
  : (isActive || isCollapsedActiveGrouper || isExpandedGrouper) ? T.text.navActive
234
236
  : hovered ? T.text.navHover
235
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.
236
241
  const iconColor =
237
242
  isDisabled ? T.icon.navDisabled
238
- : (isActive || isCollapsedActiveGrouper) ? T.icon.navActive
243
+ : (isActive || isCollapsedActiveGrouper || isExpandedGrouper) ? T.icon.navActive
239
244
  : hovered ? T.icon.navHover
240
245
  : T.icon.navBase;
241
246
 
@@ -579,7 +584,12 @@ const APP_T = {
579
584
  separator: c("stroke-static-neutral-base"),
580
585
  };
581
586
 
582
- 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);
583
593
  const [hov, setHov] = useState(false);
584
594
  const [press, setPress] = useState(false);
585
595
  const [open, setOpen] = useState(false);
@@ -612,7 +622,7 @@ export function AppSwitcher({ label, icon = "apps", home, apps = [], activeId, o
612
622
  const active = open || press;
613
623
  const fg = active ? APP_T.openFg : hov ? APP_T.hoverFg : APP_T.restFg;
614
624
  const chevron = active ? APP_T.openFg : hov ? APP_T.hoverFg : APP_T.restChevron;
615
- const homeItem = home || { id: "__home", label };
625
+ const homeItem = home || { id: "__home", label: fallbackLabel };
616
626
 
617
627
  return (
618
628
  // Full column width in both states: 218 expanded, the rail's 48 collapsed,
@@ -749,7 +759,7 @@ export function CollapseButton({ isSidebarCollapsed, onToggle, collapseIcon, exp
749
759
  // ─── SideNav (top-level container) ────────────────────────────────────────────
750
760
  // ── NavSectionLabel ──────────────────────────────────────────────────────────
751
761
  // Optional in-nav section heading. Figma: SideNav.SectionLabel (node 40006794:5975).
752
- // Tokens: Label/Section/Small/Semibold (11px/600/16px/0.6px), Foreground/Static/Neutral/Faint
762
+ // Tokens: Label/Section/Small/Semibold (11px/600/16px/0.6px), Foreground/Static/Neutral/Subtle
753
763
  // Container: h 40, padding 6 left, 6 right, 12 vertical (read 2026-11-03; was
754
764
  // 4 / 4 / 8 in neutral-base)
755
765
  // In collapsed rail: hidden, replaced by a Divider (rendered by SideNav itself, see §2.3)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloimjolopez-pathway/pathway-components",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Pathway Design System React components",
5
5
  "type": "module",
6
6
  "sideEffects": false,