@helloimjolopez-pathway/pathway-components 0.0.0-stage → 0.1.1
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/components/_shared/use-reduced-motion.jsx +49 -0
- package/components/button/button.jsx +512 -0
- package/components/icon/action-icon.jsx +155 -0
- package/components/icon/display-icon.jsx +137 -0
- package/components/icon/featured-icon.jsx +162 -0
- package/components/icon/icon.jsx +154 -0
- package/components/module-icon/module-icon.jsx +387 -0
- package/components/nav-shell/nav-shell.jsx +252 -0
- package/components/org-switcher/org-switcher.jsx +604 -0
- package/components/page-template/page-template.jsx +147 -0
- package/components/scrollbar/scrollbar.jsx +232 -0
- package/components/search/search.jsx +689 -0
- package/components/sidenav/sidenav.jsx +951 -0
- package/components/top-nav/top-nav.jsx +1003 -0
- package/package.json +27 -4
- package/README.md +0 -3
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NavShell, Pathway Design System
|
|
3
|
+
*
|
|
4
|
+
* Composes TopNav + SideNav + Shell.Main into the application frame that wraps
|
|
5
|
+
* every Amplify page. Not a Figma component set (the shell has no Figma node of
|
|
6
|
+
* its own); it is a layout composition of TopNav, SideNav, and PageTemplate.
|
|
7
|
+
*
|
|
8
|
+
* Spec: components/nav-shell/nav-shell-spec.md
|
|
9
|
+
* Figma: TopNav 40007067:6508, SideNav.Container 40004059:1375,
|
|
10
|
+
* ScreenTemplate (set) 40009709:26016
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
14
|
+
import TopNav, { DEFAULT_MODULES } from "../top-nav/top-nav.jsx";
|
|
15
|
+
import { SideNav } from "../sidenav/sidenav.jsx";
|
|
16
|
+
|
|
17
|
+
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
|
18
|
+
const TOPNAV_H = 60; // px — spec §9.3 rule 1
|
|
19
|
+
|
|
20
|
+
// SideNav widths via contextual tokens (same source as sidenav.jsx L.navW/L.navWcol)
|
|
21
|
+
const CL = (p) => `var(--contextual-layout-units-${p})`;
|
|
22
|
+
const SIDENAV_W_EXP = CL("sidenav-width-expanded");
|
|
23
|
+
const SIDENAV_W_COL = CL("sidenav-width-collapsed");
|
|
24
|
+
|
|
25
|
+
// ─── HOOK: responsive breakpoint ─────────────────────────────────────────────
|
|
26
|
+
function useBreakpoint() {
|
|
27
|
+
const get = () => {
|
|
28
|
+
if (typeof window === "undefined") return "desktop";
|
|
29
|
+
if (window.innerWidth < 768) return "mobile";
|
|
30
|
+
if (window.innerWidth < 1024) return "tablet";
|
|
31
|
+
return "desktop";
|
|
32
|
+
};
|
|
33
|
+
const [bp, setBp] = useState(get);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const h = () => setBp(get());
|
|
36
|
+
window.addEventListener("resize", h, { passive: true });
|
|
37
|
+
return () => window.removeEventListener("resize", h);
|
|
38
|
+
}, []);
|
|
39
|
+
return bp;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── COMPONENT ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* NavShell — the fixed application frame.
|
|
46
|
+
*
|
|
47
|
+
* Layout zones (always present):
|
|
48
|
+
* TopNav fixed, full width, 60px tall, z-index 100
|
|
49
|
+
* SideNav fixed, left edge, top 60px → bottom, push layout
|
|
50
|
+
* Shell.Main scrollable content area, margin-left = SideNav width
|
|
51
|
+
*
|
|
52
|
+
* Desktop: SideNav 240px expanded | 84px collapsed, user toggles
|
|
53
|
+
* Tablet: SideNav always 84px rail, toggle locked
|
|
54
|
+
* Mobile: SideNav hidden, slides in as 240px overlay on hamburger tap
|
|
55
|
+
*
|
|
56
|
+
* Props (TopNav pass-through):
|
|
57
|
+
* modules Array<{id, label, icon}> — all modules (default: DEFAULT_MODULES)
|
|
58
|
+
* activeModuleId string — which module is active
|
|
59
|
+
* org {id, name, campus, initials, logoUrl?}
|
|
60
|
+
* orgs Array<org> — OrgSwitcher dropdown list
|
|
61
|
+
* user {name, initials, email}
|
|
62
|
+
* onModuleSelect (id) => void
|
|
63
|
+
* onOrgSelect (id) => void
|
|
64
|
+
*
|
|
65
|
+
* Props (SideNav pass-through):
|
|
66
|
+
* navItems Array<SideNavItem> — flat nav items (use this OR navSections)
|
|
67
|
+
* navSections Array<{label, items}> — sectioned nav items
|
|
68
|
+
* activeNavId string — current page id
|
|
69
|
+
* onNavigate (id) => void
|
|
70
|
+
* navHeaderEnd ReactNode — slot after collapse toggle in nav header
|
|
71
|
+
* navChildren ReactNode — slot inside scroll area, after items
|
|
72
|
+
* navFooter ReactNode — slot pinned below scroll area
|
|
73
|
+
*
|
|
74
|
+
* Props (shell):
|
|
75
|
+
* defaultCollapsed boolean — initial desktop collapse state (default false)
|
|
76
|
+
* children ReactNode — page content (renders inside Shell.Main)
|
|
77
|
+
*/
|
|
78
|
+
export function NavShell({
|
|
79
|
+
// TopNav
|
|
80
|
+
modules = DEFAULT_MODULES,
|
|
81
|
+
activeModuleId = "home",
|
|
82
|
+
org,
|
|
83
|
+
orgs,
|
|
84
|
+
user,
|
|
85
|
+
onModuleSelect,
|
|
86
|
+
onOrgSelect,
|
|
87
|
+
|
|
88
|
+
// SideNav
|
|
89
|
+
navItems,
|
|
90
|
+
navSections,
|
|
91
|
+
activeNavId,
|
|
92
|
+
onNavigate,
|
|
93
|
+
navHeaderEnd = null,
|
|
94
|
+
navChildren = null,
|
|
95
|
+
navFooter = null,
|
|
96
|
+
|
|
97
|
+
// Shell
|
|
98
|
+
defaultCollapsed = false,
|
|
99
|
+
children,
|
|
100
|
+
}) {
|
|
101
|
+
const breakpoint = useBreakpoint();
|
|
102
|
+
const isMobile = breakpoint === "mobile";
|
|
103
|
+
const isTablet = breakpoint === "tablet";
|
|
104
|
+
|
|
105
|
+
// Desktop collapse state (controlled within NavShell)
|
|
106
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
107
|
+
// Mobile overlay open state
|
|
108
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
109
|
+
|
|
110
|
+
// Tablet: rail (84px) is fixed, no user toggle
|
|
111
|
+
// Mobile: always "collapsed" visually (but managed separately via mobileOpen)
|
|
112
|
+
// Desktop: user-controlled
|
|
113
|
+
const effectiveCollapsed = isTablet || isMobile ? true : collapsed;
|
|
114
|
+
|
|
115
|
+
// Shell.Main margin-left: matches current SideNav width for push layout.
|
|
116
|
+
// Mobile: no margin (SideNav is an overlay).
|
|
117
|
+
const mainMarginLeft = isMobile
|
|
118
|
+
? 0
|
|
119
|
+
: effectiveCollapsed ? SIDENAV_W_COL : SIDENAV_W_EXP;
|
|
120
|
+
|
|
121
|
+
const handleCollapseChange = useCallback((next) => {
|
|
122
|
+
if (!isTablet && !isMobile) setCollapsed(next);
|
|
123
|
+
}, [isTablet, isMobile]);
|
|
124
|
+
|
|
125
|
+
// TopNav calls this when hamburger (mobile) or collapse button (desktop) is tapped
|
|
126
|
+
const handleSideNavToggle = useCallback(() => {
|
|
127
|
+
if (isMobile) setMobileOpen(o => !o);
|
|
128
|
+
else if (!isTablet) setCollapsed(c => !c);
|
|
129
|
+
}, [isMobile, isTablet]);
|
|
130
|
+
|
|
131
|
+
// Close mobile overlay on Escape — spec §8 mobile hamburger rules
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!mobileOpen) return;
|
|
134
|
+
const h = (e) => { if (e.key === "Escape") setMobileOpen(false); };
|
|
135
|
+
document.addEventListener("keydown", h);
|
|
136
|
+
return () => document.removeEventListener("keydown", h);
|
|
137
|
+
}, [mobileOpen]);
|
|
138
|
+
|
|
139
|
+
// Also close mobile overlay when breakpoint changes away from mobile
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!isMobile) setMobileOpen(false);
|
|
142
|
+
}, [isMobile]);
|
|
143
|
+
|
|
144
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
{/* Skip navigation — first focusable element, WCAG 2.4.1 */}
|
|
149
|
+
<a
|
|
150
|
+
href="#main-content"
|
|
151
|
+
style={{
|
|
152
|
+
position: "absolute", top: -48, left: 8, zIndex: 300,
|
|
153
|
+
background: "var(--semantic-color-fill-action-primary-strong-rest)",
|
|
154
|
+
color: "var(--semantic-color-foreground-action-ghost-rest)",
|
|
155
|
+
padding: "8px 12px", borderRadius: 6,
|
|
156
|
+
fontSize: 14, fontWeight: 500, textDecoration: "none",
|
|
157
|
+
transition: "top var(--motion-duration-2) var(--motion-easing-standard)",
|
|
158
|
+
}}
|
|
159
|
+
onFocus={(e) => { e.currentTarget.style.top = "8px"; }}
|
|
160
|
+
onBlur={(e) => { e.currentTarget.style.top = "-48px"; }}
|
|
161
|
+
>
|
|
162
|
+
Skip to content
|
|
163
|
+
</a>
|
|
164
|
+
|
|
165
|
+
{/* ── TopNav: position fixed, full width, 60px ─────────────────────── */}
|
|
166
|
+
<TopNav
|
|
167
|
+
modules={modules}
|
|
168
|
+
activeModuleId={activeModuleId}
|
|
169
|
+
org={org}
|
|
170
|
+
orgs={orgs}
|
|
171
|
+
user={user}
|
|
172
|
+
breakpoint={breakpoint}
|
|
173
|
+
onModuleSelect={onModuleSelect}
|
|
174
|
+
onOrgSelect={onOrgSelect}
|
|
175
|
+
onSideNavToggle={handleSideNavToggle}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
{/* ── Mobile scrim — darkens content while SideNav overlay is open ─── */}
|
|
179
|
+
{isMobile && mobileOpen && (
|
|
180
|
+
<div
|
|
181
|
+
aria-hidden="true"
|
|
182
|
+
onClick={() => setMobileOpen(false)}
|
|
183
|
+
style={{
|
|
184
|
+
position: "fixed", inset: 0, top: TOPNAV_H,
|
|
185
|
+
background: "rgba(0,0,0,0.32)",
|
|
186
|
+
zIndex: 49,
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* ── SideNav container ────────────────────────────────────────────── */}
|
|
192
|
+
{/* Position: fixed so it stays below TopNav while the content scrolls.
|
|
193
|
+
On mobile: width controls the overlay reveal (0 → 240px); the SideNav
|
|
194
|
+
itself renders inside a clipping container and slides in from the left.
|
|
195
|
+
On desktop/tablet: overflow hidden so the SideNav's own width transition
|
|
196
|
+
stays within the container; no width needed here. */}
|
|
197
|
+
<div
|
|
198
|
+
aria-hidden={isMobile && !mobileOpen}
|
|
199
|
+
style={{
|
|
200
|
+
position: "fixed",
|
|
201
|
+
top: TOPNAV_H,
|
|
202
|
+
left: 0,
|
|
203
|
+
bottom: 0,
|
|
204
|
+
zIndex: isMobile ? 51 : 10,
|
|
205
|
+
overflow: "hidden",
|
|
206
|
+
// Mobile: clipping container, SideNav slides in via its own width
|
|
207
|
+
...(isMobile && {
|
|
208
|
+
width: mobileOpen
|
|
209
|
+
? SIDENAV_W_EXP
|
|
210
|
+
: 0,
|
|
211
|
+
transition: `width var(--motion-duration-6) var(--motion-easing-emphasized)`,
|
|
212
|
+
}),
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<SideNav
|
|
216
|
+
items={navItems}
|
|
217
|
+
sections={navSections}
|
|
218
|
+
activeId={activeNavId}
|
|
219
|
+
onNavigate={onNavigate}
|
|
220
|
+
collapsed={effectiveCollapsed}
|
|
221
|
+
onCollapseChange={handleCollapseChange}
|
|
222
|
+
hideCollapseButton={isTablet}
|
|
223
|
+
headerEnd={navHeaderEnd}
|
|
224
|
+
footer={navFooter}
|
|
225
|
+
>
|
|
226
|
+
{navChildren}
|
|
227
|
+
</SideNav>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* ── Shell.Main: scrollable content area ─────────────────────────── */}
|
|
231
|
+
{/* margin-left matches the SideNav width and transitions simultaneously.
|
|
232
|
+
spec §9.3 rule 3: never overflow hidden here. */}
|
|
233
|
+
<main
|
|
234
|
+
id="main-content"
|
|
235
|
+
role="main"
|
|
236
|
+
style={{
|
|
237
|
+
marginTop: TOPNAV_H,
|
|
238
|
+
marginLeft: mainMarginLeft,
|
|
239
|
+
minHeight: `calc(100vh - ${TOPNAV_H}px)`,
|
|
240
|
+
overflowY: "auto",
|
|
241
|
+
overflowX: "hidden",
|
|
242
|
+
background: "var(--semantic-color-fill-surface-canvas)",
|
|
243
|
+
transition: `margin-left var(--motion-duration-6) var(--motion-easing-emphasized)`,
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
{children}
|
|
247
|
+
</main>
|
|
248
|
+
</>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default NavShell;
|