@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"
|
|
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
|
|
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>
|
|
@@ -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
|
-
|
|
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/
|
|
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)
|