@mshuja288/aksuicore 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mshuja288/aksuicore",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Shared UI components and templates for Ikssam applications",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -1,9 +1,11 @@
1
1
  "use client";
2
2
 
3
- import type { ReactNode } from "react";
3
+ import { useEffect, useState, type ReactNode } from "react";
4
4
  import { SideNav, type SideNavItem } from "../primitives/SideNav";
5
5
  import { Topbar } from "../primitives/Topbar";
6
6
 
7
+ export type MobileNavMode = "drawer" | "bottom" | "hybrid";
8
+
7
9
  export type AppShellProps = {
8
10
  title: string;
9
11
  logoSrc?: string;
@@ -14,8 +16,21 @@ export type AppShellProps = {
14
16
  activeNavId: string;
15
17
  onNavSelect: (id: string) => void;
16
18
  navLabel?: string;
17
- /** When true (default), renders a mobile bottom nav from `navItems`. */
19
+ /**
20
+ * Small-screen navigation strategy.
21
+ * - `drawer` (default): hamburger opens a collapsible side menu; no crowded bottom bar
22
+ * - `bottom`: classic bottom nav from all `navItems` (fine for a few sections)
23
+ * - `hybrid`: compact bottom bar with current section + Menu that opens the drawer
24
+ */
25
+ mobileNav?: MobileNavMode;
26
+ /**
27
+ * When true, renders the mobile bottom nav.
28
+ * Defaults to true for `bottom` / `hybrid`, false for `drawer`.
29
+ */
18
30
  showBottomNav?: boolean;
31
+ /** Controlled drawer open state (optional). */
32
+ navDrawerOpen?: boolean;
33
+ onNavDrawerOpenChange?: (open: boolean) => void;
19
34
  topbarActions?: ReactNode;
20
35
  children: ReactNode;
21
36
  /** Extra class on the content/screen region */
@@ -24,7 +39,7 @@ export type AppShellProps = {
24
39
 
25
40
  /**
26
41
  * Application chrome: topbar + left side nav + content screen.
27
- * Matches aksconfigurator_ui shell layout.
42
+ * On small screens, prefers a collapsible drawer over a crowded bottom nav.
28
43
  */
29
44
  export function AppShell({
30
45
  title,
@@ -36,32 +51,140 @@ export function AppShell({
36
51
  activeNavId,
37
52
  onNavSelect,
38
53
  navLabel = "Sections",
39
- showBottomNav = true,
54
+ mobileNav = "drawer",
55
+ showBottomNav,
56
+ navDrawerOpen,
57
+ onNavDrawerOpenChange,
40
58
  topbarActions,
41
59
  children,
42
60
  screenClassName = "",
43
61
  }: AppShellProps) {
62
+ const [internalDrawerOpen, setInternalDrawerOpen] = useState(false);
63
+ const drawerControlled = navDrawerOpen !== undefined;
64
+ const drawerOpen = drawerControlled ? navDrawerOpen : internalDrawerOpen;
65
+ const useBottomNav = showBottomNav ?? (mobileNav === "bottom" || mobileNav === "hybrid");
66
+ const useDrawer = mobileNav === "drawer" || mobileNav === "hybrid";
67
+ const activeItem = navItems.find((item) => item.id === activeNavId);
68
+
69
+ function setDrawerOpen(open: boolean) {
70
+ if (!drawerControlled) {
71
+ setInternalDrawerOpen(open);
72
+ }
73
+ onNavDrawerOpenChange?.(open);
74
+ }
75
+
76
+ useEffect(() => {
77
+ if (!drawerOpen) {
78
+ return;
79
+ }
80
+
81
+ function onKeyDown(event: KeyboardEvent) {
82
+ if (event.key === "Escape") {
83
+ if (!drawerControlled) {
84
+ setInternalDrawerOpen(false);
85
+ }
86
+ onNavDrawerOpenChange?.(false);
87
+ }
88
+ }
89
+
90
+ document.addEventListener("keydown", onKeyDown);
91
+ const previousOverflow = document.body.style.overflow;
92
+ document.body.style.overflow = "hidden";
93
+ return () => {
94
+ document.removeEventListener("keydown", onKeyDown);
95
+ document.body.style.overflow = previousOverflow;
96
+ };
97
+ }, [drawerControlled, drawerOpen, onNavDrawerOpenChange]);
98
+
99
+ function selectNav(id: string) {
100
+ onNavSelect(id);
101
+ setDrawerOpen(false);
102
+ }
103
+
44
104
  return (
45
- <div className="aksui-root app-shell">
46
- <Topbar title={title} logoSrc={logoSrc} logoAlt={logoAlt} username={username} onLogout={onLogout}>
105
+ <div
106
+ className={[
107
+ "aksui-root",
108
+ "app-shell",
109
+ `mobile-nav-${mobileNav}`,
110
+ useBottomNav ? "has-bottom-nav" : "",
111
+ drawerOpen ? "nav-drawer-open" : "",
112
+ ]
113
+ .filter(Boolean)
114
+ .join(" ")}
115
+ >
116
+ <Topbar
117
+ title={title}
118
+ logoSrc={logoSrc}
119
+ logoAlt={logoAlt}
120
+ username={username}
121
+ onLogout={onLogout}
122
+ showMenuButton={useDrawer}
123
+ menuOpen={drawerOpen}
124
+ onMenuClick={() => setDrawerOpen(!drawerOpen)}
125
+ >
47
126
  {topbarActions}
48
127
  </Topbar>
128
+
49
129
  <div className="app-body">
50
130
  <SideNav items={navItems} activeId={activeNavId} onSelect={onNavSelect} label={navLabel} />
51
131
  <div className={["screen", screenClassName].filter(Boolean).join(" ")}>{children}</div>
52
132
  </div>
53
- {showBottomNav ? (
133
+
134
+ {useDrawer ? (
135
+ <>
136
+ <button
137
+ type="button"
138
+ className={drawerOpen ? "nav-drawer-backdrop open" : "nav-drawer-backdrop"}
139
+ aria-label="Close navigation menu"
140
+ tabIndex={drawerOpen ? 0 : -1}
141
+ onClick={() => setDrawerOpen(false)}
142
+ />
143
+ <aside
144
+ className={drawerOpen ? "nav-drawer open" : "nav-drawer"}
145
+ aria-label={navLabel}
146
+ aria-hidden={!drawerOpen}
147
+ >
148
+ <div className="nav-drawer-head">
149
+ <p className="nav-drawer-title">{navLabel}</p>
150
+ <button type="button" className="ghost nav-drawer-close" onClick={() => setDrawerOpen(false)}>
151
+ Close
152
+ </button>
153
+ </div>
154
+ <SideNav
155
+ className="nav-drawer-nav"
156
+ items={navItems}
157
+ activeId={activeNavId}
158
+ onSelect={selectNav}
159
+ label={navLabel}
160
+ />
161
+ </aside>
162
+ </>
163
+ ) : null}
164
+
165
+ {useBottomNav ? (
54
166
  <nav className="bottom-nav" aria-label={navLabel}>
55
- {navItems.map((item) => (
56
- <button
57
- key={item.id}
58
- type="button"
59
- className={activeNavId === item.id ? "active" : ""}
60
- onClick={() => onNavSelect(item.id)}
61
- >
62
- {item.short ?? item.label}
63
- </button>
64
- ))}
167
+ {mobileNav === "hybrid" ? (
168
+ <>
169
+ <button type="button" className="active bottom-nav-current" disabled>
170
+ {activeItem?.short ?? activeItem?.label ?? "Section"}
171
+ </button>
172
+ <button type="button" className="bottom-nav-menu" onClick={() => setDrawerOpen(true)}>
173
+ Menu
174
+ </button>
175
+ </>
176
+ ) : (
177
+ navItems.map((item) => (
178
+ <button
179
+ key={item.id}
180
+ type="button"
181
+ className={activeNavId === item.id ? "active" : ""}
182
+ onClick={() => onNavSelect(item.id)}
183
+ >
184
+ {item.short ?? item.label}
185
+ </button>
186
+ ))
187
+ )}
65
188
  </nav>
66
189
  ) : null}
67
190
  </div>