@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/CHANGELOG.md +24 -0
- package/dist/aksui.css +508 -17
- package/dist/index.cjs +727 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -9
- package/dist/index.d.ts +49 -9
- package/dist/index.js +727 -219
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/patterns/AppShell.tsx +140 -17
- package/src/patterns/RecordWorkspace.tsx +656 -189
- package/src/patterns/index.ts +2 -1
- package/src/primitives/SideNav.tsx +3 -1
- package/src/primitives/Topbar.tsx +38 -8
- package/src/theme/patterns.css +343 -11
- package/src/theme/primitives.css +11 -1
- package/src/theme/shell.css +154 -5
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
onClick={() =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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>
|