@fayz-ai/ui 0.5.0 → 0.6.6
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/dist/{chunk-7TBDPNLY.cjs → chunk-5QFB6AIQ.cjs} +40 -11
- package/dist/chunk-5QFB6AIQ.cjs.map +1 -0
- package/dist/{chunk-GE6XUREM.cjs → chunk-DJHS2XRW.cjs} +185 -21
- package/dist/chunk-DJHS2XRW.cjs.map +1 -0
- package/dist/{chunk-CZ6X6PZT.js → chunk-LO5QZO7F.js} +192 -28
- package/dist/chunk-LO5QZO7F.js.map +1 -0
- package/dist/{chunk-GFH5HOCO.js → chunk-XLBGB7IW.js} +40 -12
- package/dist/chunk-XLBGB7IW.js.map +1 -0
- package/dist/dashboard/DashboardCanvas.d.ts +5 -1
- package/dist/dashboard/DashboardCanvas.d.ts.map +1 -1
- package/dist/dashboard/GoldCard.d.ts +19 -0
- package/dist/dashboard/GoldCard.d.ts.map +1 -0
- package/dist/dashboard/KpiCard.d.ts.map +1 -1
- package/dist/dashboard/index.cjs +28 -24
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/index.cjs +46 -42
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/layout/AppShell.d.ts +27 -6
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Topbar.d.ts.map +1 -1
- package/dist/layout/index.cjs +17 -17
- package/dist/layout/index.d.ts +1 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +1 -1
- package/dist/tailwind.cjs +30 -0
- package/dist/tailwind.cjs.map +1 -0
- package/dist/tailwind.d.ts +19 -0
- package/dist/tailwind.d.ts.map +1 -0
- package/dist/tailwind.js +28 -0
- package/dist/tailwind.js.map +1 -0
- package/package.json +9 -6
- package/src/dashboard/DashboardCanvas.tsx +13 -2
- package/src/dashboard/GoldCard.tsx +38 -0
- package/src/dashboard/KpiCard.tsx +4 -2
- package/src/dashboard/index.ts +1 -0
- package/src/index.ts +6 -0
- package/src/layout/AppShell.tsx +240 -20
- package/src/layout/ModulePage.tsx +2 -2
- package/src/layout/Sidebar.tsx +32 -6
- package/src/layout/Topbar.tsx +46 -17
- package/src/layout/index.ts +9 -1
- package/src/tailwind.ts +43 -0
- package/dist/chunk-7TBDPNLY.cjs.map +0 -1
- package/dist/chunk-CZ6X6PZT.js.map +0 -1
- package/dist/chunk-GE6XUREM.cjs.map +0 -1
- package/dist/chunk-GFH5HOCO.js.map +0 -1
package/src/layout/AppShell.tsx
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Menu,
|
|
4
|
+
Home,
|
|
5
|
+
Receipt,
|
|
6
|
+
ArrowLeftRight,
|
|
7
|
+
Calendar,
|
|
8
|
+
CreditCard,
|
|
9
|
+
Settings,
|
|
10
|
+
MessageCircle,
|
|
11
|
+
Wallet,
|
|
12
|
+
PiggyBank,
|
|
13
|
+
Plus,
|
|
14
|
+
type LucideIcon,
|
|
15
|
+
} from 'lucide-react'
|
|
3
16
|
import { Sidebar, type NavigationItem, type SidebarUser, type SidebarProps } from './Sidebar'
|
|
4
17
|
import { Topbar } from './Topbar'
|
|
5
18
|
import { SaveBar, SaveBarProvider } from './SaveBar'
|
|
@@ -35,6 +48,41 @@ export interface AppShellUser {
|
|
|
35
48
|
email: string
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Bottom-nav item model
|
|
53
|
+
//
|
|
54
|
+
// Two kinds share the mobile tab bar:
|
|
55
|
+
// - route (default): a normal tab that navigates to `route`.
|
|
56
|
+
// - action: a NON-route item rendered as a raised, elevated circular center
|
|
57
|
+
// button (Mobills style). It fires `onBottomNavAction(id)` instead of
|
|
58
|
+
// navigating — e.g. a global quick-add "+".
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
export interface BottomNavRouteItem {
|
|
62
|
+
kind?: 'route'
|
|
63
|
+
label: string
|
|
64
|
+
icon: string
|
|
65
|
+
route: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BottomNavActionItem {
|
|
69
|
+
kind: 'action'
|
|
70
|
+
/** Identifier passed to onBottomNavAction. */
|
|
71
|
+
id: string
|
|
72
|
+
icon: string
|
|
73
|
+
/** Optional accessible label. */
|
|
74
|
+
label?: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type BottomNavItem = BottomNavRouteItem | BottomNavActionItem
|
|
78
|
+
|
|
79
|
+
/** Mobile header treatment (small screens, <md). Desktop is unaffected.
|
|
80
|
+
* - 'minimal' : a small top header bar (the default / today's behavior).
|
|
81
|
+
* - 'transparent' : NO header bar; content is edge-to-edge with a floating
|
|
82
|
+
* profile avatar pinned top-right (calls onProfile).
|
|
83
|
+
* - 'hidden' : no header bar and no floating avatar. */
|
|
84
|
+
export type MobileHeaderVariant = 'transparent' | 'minimal' | 'hidden'
|
|
85
|
+
|
|
38
86
|
export interface AppShellProps {
|
|
39
87
|
/** Layout variant */
|
|
40
88
|
variant: 'sidebar' | 'topbar' | 'minimal'
|
|
@@ -58,7 +106,11 @@ export interface AppShellProps {
|
|
|
58
106
|
topbarEnd?: React.ReactNode
|
|
59
107
|
sidebarTopContent?: React.ReactNode
|
|
60
108
|
sidebarFooterContent?: React.ReactNode
|
|
61
|
-
bottomNav?:
|
|
109
|
+
bottomNav?: BottomNavItem[]
|
|
110
|
+
/** Fired when a bottom-nav `action` item (e.g. the center "+") is tapped. */
|
|
111
|
+
onBottomNavAction?: (id: string) => void
|
|
112
|
+
/** Mobile header treatment (<md). Default: 'minimal'. Desktop unaffected. */
|
|
113
|
+
mobileHeader?: MobileHeaderVariant
|
|
62
114
|
/** Rendered in sidebar bottom user slot */
|
|
63
115
|
userMenuSlot?: React.ReactNode
|
|
64
116
|
/** Rendered in sidebar bottom notification slot */
|
|
@@ -93,6 +145,8 @@ function SidebarLayout({
|
|
|
93
145
|
userMenuSlot,
|
|
94
146
|
notificationSlot,
|
|
95
147
|
unreadCount = 0,
|
|
148
|
+
hasBottomNav = false,
|
|
149
|
+
mobileHeader = 'minimal',
|
|
96
150
|
}: {
|
|
97
151
|
navigation?: NavigationItem[]
|
|
98
152
|
logo?: React.ReactNode
|
|
@@ -115,6 +169,11 @@ function SidebarLayout({
|
|
|
115
169
|
userMenuSlot?: React.ReactNode
|
|
116
170
|
notificationSlot?: React.ReactNode
|
|
117
171
|
unreadCount?: number
|
|
172
|
+
/** When a mobile bottom-nav bar is present, the mobile hamburger is
|
|
173
|
+
* suppressed so the bottom nav is the primary mobile navigation. */
|
|
174
|
+
hasBottomNav?: boolean
|
|
175
|
+
/** Mobile header treatment (<md). Desktop keeps its top bar untouched. */
|
|
176
|
+
mobileHeader?: MobileHeaderVariant
|
|
118
177
|
}) {
|
|
119
178
|
const sidebarCollapsed = useLayoutStore((s) => s.sidebarCollapsed)
|
|
120
179
|
const setSidebarCollapsed = useLayoutStore((s) => s.setSidebarCollapsed)
|
|
@@ -126,7 +185,13 @@ function SidebarLayout({
|
|
|
126
185
|
<SaveBarProvider>
|
|
127
186
|
<ModuleHeaderSlotContext.Provider value={headerSlot}>
|
|
128
187
|
<ModuleHeaderActionsSlotContext.Provider value={actionsSlot}>
|
|
129
|
-
<div className=
|
|
188
|
+
<div className={cn(
|
|
189
|
+
'flex h-screen overflow-hidden bg-background',
|
|
190
|
+
// Frame mode: the inset gap around the framed card takes the sidebar (menu)
|
|
191
|
+
// color, so the white card reads as a panel floating on the same tone as the
|
|
192
|
+
// left menu. The card's own md:m-2 margin reveals this behind it.
|
|
193
|
+
frame && 'md:bg-sidebar',
|
|
194
|
+
)}>
|
|
130
195
|
{/* Desktop Sidebar — always flush / full-height (GoHighLevel style) */}
|
|
131
196
|
<div className="hidden md:flex">
|
|
132
197
|
<Sidebar
|
|
@@ -158,16 +223,27 @@ function SidebarLayout({
|
|
|
158
223
|
'relative flex flex-1 flex-col overflow-hidden',
|
|
159
224
|
frame && 'md:m-2 md:rounded-xl md:border md:border-border md:bg-card'
|
|
160
225
|
)}>
|
|
161
|
-
{/* Top bar — mobile hamburger + module header slot (title + sub-nav) + actions
|
|
162
|
-
|
|
226
|
+
{/* Top bar — mobile hamburger + module header slot (title + sub-nav) + actions.
|
|
227
|
+
On mobile the bar is suppressed for the 'transparent' / 'hidden' header
|
|
228
|
+
treatments (content goes edge-to-edge); desktop keeps it always. The
|
|
229
|
+
portal targets below stay mounted (display:none on mobile) so module
|
|
230
|
+
pages can still portal their sub-nav on desktop without crashing. */}
|
|
231
|
+
<div className={cn(
|
|
232
|
+
'flex min-h-14 shrink-0 items-center justify-between gap-3 border-b border-border bg-card px-4 py-2',
|
|
233
|
+
mobileHeader !== 'minimal' && 'hidden md:flex',
|
|
234
|
+
)}>
|
|
163
235
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
236
|
+
{/* Mobile hamburger — suppressed when a bottom-nav bar owns mobile
|
|
237
|
+
navigation (mobile-first apps). Desktop sidebar is untouched. */}
|
|
238
|
+
{!hasBottomNav && (
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => setMobileMenuOpen(true)}
|
|
241
|
+
className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent md:hidden"
|
|
242
|
+
aria-label="Open menu"
|
|
243
|
+
>
|
|
244
|
+
<Menu className="h-5 w-5" />
|
|
245
|
+
</button>
|
|
246
|
+
)}
|
|
171
247
|
{pageTitle && <h2 className="font-semibold text-lg">{pageTitle}</h2>}
|
|
172
248
|
{topbarStart}
|
|
173
249
|
{/* Module pages (tabs variant) portal their sub-nav here */}
|
|
@@ -181,7 +257,7 @@ function SidebarLayout({
|
|
|
181
257
|
</div>
|
|
182
258
|
|
|
183
259
|
{/* Page content */}
|
|
184
|
-
<main className="flex-1 overflow-y-auto">
|
|
260
|
+
<main className="flex-1 overflow-y-auto pb-16 md:pb-0">
|
|
185
261
|
{children}
|
|
186
262
|
</main>
|
|
187
263
|
|
|
@@ -236,6 +312,7 @@ function TopbarLayout({
|
|
|
236
312
|
currentPath,
|
|
237
313
|
topbarStart,
|
|
238
314
|
topbarEnd,
|
|
315
|
+
hasBottomNav = false,
|
|
239
316
|
}: {
|
|
240
317
|
navigation?: NavigationItem[]
|
|
241
318
|
logo?: React.ReactNode
|
|
@@ -251,6 +328,9 @@ function TopbarLayout({
|
|
|
251
328
|
currentPath?: string
|
|
252
329
|
topbarStart?: React.ReactNode
|
|
253
330
|
topbarEnd?: React.ReactNode
|
|
331
|
+
/** When a mobile bottom-nav bar is present, the mobile hamburger is
|
|
332
|
+
* suppressed so the bottom nav is the primary mobile navigation. */
|
|
333
|
+
hasBottomNav?: boolean
|
|
254
334
|
}) {
|
|
255
335
|
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false)
|
|
256
336
|
|
|
@@ -271,10 +351,12 @@ function TopbarLayout({
|
|
|
271
351
|
userMenuExtras={userMenuExtras}
|
|
272
352
|
leftContent={topbarStart}
|
|
273
353
|
rightContent={topbarEnd}
|
|
274
|
-
onMenuClick={() => setMobileMenuOpen(true)}
|
|
354
|
+
onMenuClick={hasBottomNav ? undefined : () => setMobileMenuOpen(true)}
|
|
275
355
|
/>
|
|
276
|
-
{/* Frame inset/border only from md up, so mobile stays edge-to-edge.
|
|
277
|
-
|
|
356
|
+
{/* Frame inset/border only from md up, so mobile stays edge-to-edge. The
|
|
357
|
+
inset gap behind the framed card is the header color (bg-sidebar), so the
|
|
358
|
+
white card reads as a panel floating on the same navy as the topbar. */}
|
|
359
|
+
<main className={cn('flex-1 overflow-y-auto pb-16 md:pb-0', frame && 'md:bg-sidebar md:p-2')}>
|
|
278
360
|
<div className={cn('min-h-full', frame && 'md:rounded-xl md:border md:border-border md:bg-card')}>
|
|
279
361
|
{children}
|
|
280
362
|
</div>
|
|
@@ -345,7 +427,7 @@ function MinimalLayout({
|
|
|
345
427
|
</div>
|
|
346
428
|
{headerEnd && <div className="flex items-center gap-2">{headerEnd}</div>}
|
|
347
429
|
</header>
|
|
348
|
-
<main className="flex-1 overflow-y-auto">
|
|
430
|
+
<main className="flex-1 overflow-y-auto pb-16 md:pb-0">
|
|
349
431
|
{children}
|
|
350
432
|
</main>
|
|
351
433
|
</div>
|
|
@@ -426,6 +508,111 @@ function MobileOverlay({ onClose, onNavigate, ...sidebarProps }: MobileOverlayPr
|
|
|
426
508
|
)
|
|
427
509
|
}
|
|
428
510
|
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Mobile bottom tab bar — rendered for every variant on small screens.
|
|
513
|
+
// ui must NOT import from saas, so the icon lookup is a small local map (a
|
|
514
|
+
// mirror of the saas icon convention). Unknown icon names fall back to Home.
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
|
|
517
|
+
const BOTTOM_NAV_ICONS: Record<string, LucideIcon> = {
|
|
518
|
+
Home,
|
|
519
|
+
Receipt,
|
|
520
|
+
ArrowLeftRight,
|
|
521
|
+
Calendar,
|
|
522
|
+
CreditCard,
|
|
523
|
+
Settings,
|
|
524
|
+
Menu,
|
|
525
|
+
MessageCircle,
|
|
526
|
+
Wallet,
|
|
527
|
+
PiggyBank,
|
|
528
|
+
Plus,
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Floating profile avatar — mobile-only (md:hidden), pinned top-right over the
|
|
532
|
+
// content. Used with the 'transparent' mobile header treatment so there is no
|
|
533
|
+
// header bar but the user still has a one-tap route into their profile.
|
|
534
|
+
function FloatingProfileAvatar({ user, onClick }: { user?: AppShellUser; onClick?: () => void }) {
|
|
535
|
+
if (!user) return null
|
|
536
|
+
const label = user.fullName || user.email
|
|
537
|
+
return (
|
|
538
|
+
<button
|
|
539
|
+
type="button"
|
|
540
|
+
onClick={onClick}
|
|
541
|
+
aria-label="Profile"
|
|
542
|
+
className="fixed right-3 top-3 z-40 md:hidden"
|
|
543
|
+
>
|
|
544
|
+
{user.avatarUrl ? (
|
|
545
|
+
<img
|
|
546
|
+
src={user.avatarUrl}
|
|
547
|
+
alt=""
|
|
548
|
+
className="h-9 w-9 rounded-full object-cover shadow-md ring-2 ring-background"
|
|
549
|
+
/>
|
|
550
|
+
) : (
|
|
551
|
+
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground shadow-md ring-2 ring-background">
|
|
552
|
+
{(label || '?').charAt(0).toUpperCase()}
|
|
553
|
+
</span>
|
|
554
|
+
)}
|
|
555
|
+
</button>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function MobileBottomNav({
|
|
560
|
+
items,
|
|
561
|
+
currentPath,
|
|
562
|
+
onNavigate,
|
|
563
|
+
onAction,
|
|
564
|
+
}: {
|
|
565
|
+
items: NonNullable<AppShellProps['bottomNav']>
|
|
566
|
+
currentPath?: string
|
|
567
|
+
onNavigate?: (route: string) => void
|
|
568
|
+
onAction?: (id: string) => void
|
|
569
|
+
}) {
|
|
570
|
+
const path = currentPath ?? ''
|
|
571
|
+
return (
|
|
572
|
+
<nav className="fixed inset-x-0 bottom-0 z-50 flex h-16 items-center justify-around border-t border-border bg-background md:hidden">
|
|
573
|
+
{items.map((item) => {
|
|
574
|
+
// Elevated center ACTION button (Mobills style) — raised above the bar,
|
|
575
|
+
// circular, primary-filled; fires onBottomNavAction instead of routing.
|
|
576
|
+
if (item.kind === 'action') {
|
|
577
|
+
const ActionIcon = BOTTOM_NAV_ICONS[item.icon] ?? Plus
|
|
578
|
+
return (
|
|
579
|
+
<div key={item.id} className="flex flex-1 items-start justify-center">
|
|
580
|
+
<button
|
|
581
|
+
type="button"
|
|
582
|
+
onClick={() => onAction?.(item.id)}
|
|
583
|
+
aria-label={item.label ?? 'Add'}
|
|
584
|
+
className="-mt-6 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg shadow-primary/30 ring-4 ring-background transition-transform active:scale-95"
|
|
585
|
+
>
|
|
586
|
+
<ActionIcon className="h-6 w-6" />
|
|
587
|
+
</button>
|
|
588
|
+
</div>
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
const isActive =
|
|
592
|
+
item.route === '/'
|
|
593
|
+
? path === '/' || path === ''
|
|
594
|
+
: path === item.route || path.startsWith(item.route + '/')
|
|
595
|
+
const Icon = BOTTOM_NAV_ICONS[item.icon] ?? Home
|
|
596
|
+
return (
|
|
597
|
+
<button
|
|
598
|
+
key={item.route}
|
|
599
|
+
type="button"
|
|
600
|
+
onClick={() => onNavigate?.(item.route)}
|
|
601
|
+
className={cn(
|
|
602
|
+
'flex flex-1 flex-col items-center justify-center gap-0.5 px-1 py-1 transition-colors',
|
|
603
|
+
isActive ? 'text-primary' : 'text-muted-foreground',
|
|
604
|
+
)}
|
|
605
|
+
aria-current={isActive ? 'page' : undefined}
|
|
606
|
+
>
|
|
607
|
+
<Icon className="h-5 w-5" />
|
|
608
|
+
<span className="text-xs">{item.label}</span>
|
|
609
|
+
</button>
|
|
610
|
+
)
|
|
611
|
+
})}
|
|
612
|
+
</nav>
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
|
|
429
616
|
// ---------------------------------------------------------------------------
|
|
430
617
|
// AppShell — main entry point
|
|
431
618
|
// ---------------------------------------------------------------------------
|
|
@@ -450,13 +637,32 @@ export function AppShell({
|
|
|
450
637
|
topbarEnd,
|
|
451
638
|
sidebarTopContent,
|
|
452
639
|
sidebarFooterContent,
|
|
640
|
+
bottomNav,
|
|
641
|
+
onBottomNavAction,
|
|
642
|
+
mobileHeader = 'minimal',
|
|
453
643
|
userMenuSlot,
|
|
454
644
|
notificationSlot,
|
|
455
645
|
unreadCount = 0,
|
|
456
646
|
}: AppShellProps) {
|
|
647
|
+
const hasBottomNav = !!bottomNav?.length
|
|
648
|
+
const bottomBar = hasBottomNav ? (
|
|
649
|
+
<MobileBottomNav
|
|
650
|
+
items={bottomNav!}
|
|
651
|
+
currentPath={currentPath}
|
|
652
|
+
onNavigate={onNavigate}
|
|
653
|
+
onAction={onBottomNavAction}
|
|
654
|
+
/>
|
|
655
|
+
) : null
|
|
656
|
+
// Transparent mobile header → no bar, a floating avatar top-right instead.
|
|
657
|
+
const floatingAvatar =
|
|
658
|
+
mobileHeader === 'transparent' && user ? (
|
|
659
|
+
<FloatingProfileAvatar user={user} onClick={onProfile} />
|
|
660
|
+
) : null
|
|
661
|
+
|
|
662
|
+
let layoutElement: React.ReactNode
|
|
457
663
|
switch (variant) {
|
|
458
664
|
case 'sidebar':
|
|
459
|
-
|
|
665
|
+
layoutElement = (
|
|
460
666
|
<SidebarLayout
|
|
461
667
|
navigation={navigation}
|
|
462
668
|
logo={logo}
|
|
@@ -478,13 +684,16 @@ export function AppShell({
|
|
|
478
684
|
userMenuSlot={userMenuSlot}
|
|
479
685
|
notificationSlot={notificationSlot}
|
|
480
686
|
unreadCount={unreadCount}
|
|
687
|
+
hasBottomNav={hasBottomNav}
|
|
688
|
+
mobileHeader={mobileHeader}
|
|
481
689
|
>
|
|
482
690
|
{children}
|
|
483
691
|
</SidebarLayout>
|
|
484
692
|
)
|
|
693
|
+
break
|
|
485
694
|
|
|
486
695
|
case 'topbar':
|
|
487
|
-
|
|
696
|
+
layoutElement = (
|
|
488
697
|
<TopbarLayout
|
|
489
698
|
navigation={navigation}
|
|
490
699
|
logo={logo}
|
|
@@ -499,21 +708,32 @@ export function AppShell({
|
|
|
499
708
|
currentPath={currentPath}
|
|
500
709
|
topbarStart={topbarStart}
|
|
501
710
|
topbarEnd={topbarEnd}
|
|
711
|
+
hasBottomNav={hasBottomNav}
|
|
502
712
|
>
|
|
503
713
|
{children}
|
|
504
714
|
</TopbarLayout>
|
|
505
715
|
)
|
|
716
|
+
break
|
|
506
717
|
|
|
507
718
|
case 'minimal':
|
|
508
|
-
|
|
719
|
+
layoutElement = (
|
|
509
720
|
<MinimalLayout logo={logo} user={user} headerEnd={topbarEnd}>
|
|
510
721
|
{children}
|
|
511
722
|
</MinimalLayout>
|
|
512
723
|
)
|
|
724
|
+
break
|
|
513
725
|
|
|
514
726
|
default: {
|
|
515
727
|
const _exhaustive: never = variant
|
|
516
728
|
return null
|
|
517
729
|
}
|
|
518
730
|
}
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<>
|
|
734
|
+
{layoutElement}
|
|
735
|
+
{floatingAvatar}
|
|
736
|
+
{bottomBar}
|
|
737
|
+
</>
|
|
738
|
+
)
|
|
519
739
|
}
|
|
@@ -139,7 +139,7 @@ function MobileTabs({ nav }: { nav: ModuleNavItem[] }) {
|
|
|
139
139
|
|
|
140
140
|
return (
|
|
141
141
|
<div className="md:hidden -mx-1 mb-4 no-print">
|
|
142
|
-
<div ref={scrollRef} className="flex gap-1 px-1 pb-2 overflow-x-auto scrollbar-
|
|
142
|
+
<div ref={scrollRef} className="flex gap-1 px-1 pb-2 overflow-x-auto scrollbar-none">
|
|
143
143
|
{nav.map((item) => {
|
|
144
144
|
const Icon = item.icon ? (ICON_MAP[item.icon] ?? null) : null
|
|
145
145
|
const hasChildren = item.children && item.children.length > 0
|
|
@@ -340,7 +340,7 @@ function ModuleHeader({ nav }: { nav: ModuleNavItem[] }) {
|
|
|
340
340
|
|
|
341
341
|
const content = (
|
|
342
342
|
<div className="flex w-full min-w-0 items-center gap-4">
|
|
343
|
-
<div className="flex min-w-0 items-center gap-0.5 overflow-x-auto scrollbar-
|
|
343
|
+
<div className="flex min-w-0 items-center gap-0.5 overflow-x-auto scrollbar-none">
|
|
344
344
|
{nav.map((item) => {
|
|
345
345
|
const Icon = item.icon ? (ICON_MAP[item.icon] ?? null) : null
|
|
346
346
|
const hasChildren = item.children && item.children.length > 0
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -7,40 +7,53 @@ import {
|
|
|
7
7
|
CreditCard,
|
|
8
8
|
Bell,
|
|
9
9
|
Calendar,
|
|
10
|
+
CalendarClock,
|
|
10
11
|
Package,
|
|
12
|
+
Activity,
|
|
11
13
|
BarChart3,
|
|
12
14
|
FileText,
|
|
13
15
|
Mail,
|
|
14
16
|
PanelLeftClose,
|
|
15
17
|
PanelLeft,
|
|
16
18
|
Search,
|
|
19
|
+
Shield,
|
|
17
20
|
DollarSign,
|
|
18
21
|
Megaphone,
|
|
22
|
+
Receipt,
|
|
19
23
|
ShoppingCart,
|
|
24
|
+
ShoppingBag,
|
|
20
25
|
Target,
|
|
21
26
|
Wrench,
|
|
22
27
|
ClipboardList,
|
|
28
|
+
ListChecks,
|
|
23
29
|
Briefcase,
|
|
24
30
|
UserCog,
|
|
25
31
|
BookOpen,
|
|
32
|
+
BookOpenCheck,
|
|
26
33
|
MessageCircle,
|
|
27
34
|
Globe,
|
|
28
35
|
Percent,
|
|
29
36
|
Tag,
|
|
37
|
+
Tags,
|
|
30
38
|
Camera,
|
|
31
39
|
UtensilsCrossed,
|
|
32
40
|
MapPin,
|
|
41
|
+
Map,
|
|
33
42
|
Handshake,
|
|
34
43
|
Contact,
|
|
35
44
|
Building2,
|
|
36
45
|
Filter,
|
|
37
46
|
Plus,
|
|
38
47
|
List,
|
|
48
|
+
ListPlus,
|
|
39
49
|
ChevronDown,
|
|
50
|
+
Ban,
|
|
51
|
+
Clock,
|
|
40
52
|
Dog,
|
|
41
53
|
Cat,
|
|
42
54
|
PawPrint,
|
|
43
55
|
Heart,
|
|
56
|
+
FolderOpen,
|
|
44
57
|
LayoutTemplate,
|
|
45
58
|
LeafyGreen,
|
|
46
59
|
Apple,
|
|
@@ -49,7 +62,12 @@ import {
|
|
|
49
62
|
PartyPopper,
|
|
50
63
|
Radio,
|
|
51
64
|
CalendarDays,
|
|
65
|
+
CalendarCheck2,
|
|
66
|
+
CalendarX,
|
|
67
|
+
BadgeDollarSign,
|
|
52
68
|
Banknote,
|
|
69
|
+
Landmark,
|
|
70
|
+
CircleDollarSign,
|
|
53
71
|
Layers,
|
|
54
72
|
Music,
|
|
55
73
|
Eye,
|
|
@@ -61,8 +79,16 @@ import {
|
|
|
61
79
|
Award,
|
|
62
80
|
LayoutDashboard,
|
|
63
81
|
MessageSquare,
|
|
82
|
+
Inbox,
|
|
64
83
|
Star,
|
|
84
|
+
TrendingUp,
|
|
85
|
+
UserCheck,
|
|
86
|
+
UserPlus,
|
|
87
|
+
UserX,
|
|
65
88
|
Workflow,
|
|
89
|
+
Boxes,
|
|
90
|
+
Clock3,
|
|
91
|
+
FileCheck2,
|
|
66
92
|
type LucideIcon,
|
|
67
93
|
} from 'lucide-react'
|
|
68
94
|
import { cn } from '../utils/cn'
|
|
@@ -112,13 +138,13 @@ export interface SidebarProps {
|
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
const ICON_MAP: Record<string, LucideIcon> = {
|
|
115
|
-
Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
|
|
141
|
+
Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
|
|
116
142
|
FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench,
|
|
117
|
-
ClipboardList, Briefcase, UserCog, BookOpen, MessageCircle, Globe,
|
|
118
|
-
Percent, Tag, Camera, UtensilsCrossed, MapPin, Handshake, Contact,
|
|
119
|
-
Building2, Filter, Plus, List, Search, Dog, Cat, PawPrint, Heart, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
120
|
-
PartyPopper, Radio, CalendarDays, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
121
|
-
Zap, Award, LayoutDashboard, MessageSquare, Star, Workflow,
|
|
143
|
+
Receipt, ShoppingBag, ClipboardList, ListChecks, Briefcase, UserCog, BookOpen, BookOpenCheck, MessageCircle, Globe,
|
|
144
|
+
Percent, Tag, Tags, Camera, UtensilsCrossed, MapPin, Map, Handshake, Contact,
|
|
145
|
+
Building2, Filter, Plus, List, ListPlus, Search, Shield, Ban, Clock, Dog, Cat, PawPrint, Heart, FolderOpen, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
146
|
+
PartyPopper, Radio, CalendarDays, CalendarCheck2, CalendarX, BadgeDollarSign, Banknote, Landmark, CircleDollarSign, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
147
|
+
Zap, Award, LayoutDashboard, MessageSquare, Inbox, Star, TrendingUp, UserCheck, UserPlus, UserX, Workflow, Boxes, Clock3, FileCheck2,
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
export { ICON_MAP }
|
package/src/layout/Topbar.tsx
CHANGED
|
@@ -11,23 +11,29 @@ import {
|
|
|
11
11
|
CreditCard,
|
|
12
12
|
Bell,
|
|
13
13
|
Calendar,
|
|
14
|
+
CalendarClock,
|
|
14
15
|
Package,
|
|
16
|
+
Activity,
|
|
15
17
|
BarChart3,
|
|
16
18
|
FileText,
|
|
17
19
|
Mail,
|
|
18
20
|
DollarSign,
|
|
19
21
|
Megaphone,
|
|
20
22
|
ShoppingCart,
|
|
23
|
+
ShoppingBag,
|
|
21
24
|
Target,
|
|
22
25
|
Wrench,
|
|
23
26
|
ClipboardList,
|
|
27
|
+
ClipboardCheck,
|
|
24
28
|
Briefcase,
|
|
25
29
|
UserCog,
|
|
26
30
|
BookOpen,
|
|
31
|
+
BookOpenCheck,
|
|
27
32
|
MessageCircle,
|
|
28
33
|
Globe,
|
|
29
34
|
Percent,
|
|
30
35
|
Tag,
|
|
36
|
+
Tags,
|
|
31
37
|
Camera,
|
|
32
38
|
UtensilsCrossed,
|
|
33
39
|
MapPin,
|
|
@@ -35,7 +41,11 @@ import {
|
|
|
35
41
|
Contact,
|
|
36
42
|
Building2,
|
|
37
43
|
Plus,
|
|
44
|
+
Shield,
|
|
38
45
|
List,
|
|
46
|
+
ListChecks,
|
|
47
|
+
FileCheck2,
|
|
48
|
+
FolderOpen,
|
|
39
49
|
Dog,
|
|
40
50
|
Cat,
|
|
41
51
|
PawPrint,
|
|
@@ -64,18 +74,32 @@ import {
|
|
|
64
74
|
SlidersHorizontal,
|
|
65
75
|
TableProperties,
|
|
66
76
|
UserPlus,
|
|
77
|
+
UserCheck,
|
|
78
|
+
UserX,
|
|
67
79
|
TreePalm,
|
|
80
|
+
Boxes,
|
|
81
|
+
Box,
|
|
68
82
|
PartyPopper,
|
|
69
83
|
Radio,
|
|
70
84
|
CalendarDays,
|
|
85
|
+
CalendarCheck2,
|
|
86
|
+
CalendarX,
|
|
87
|
+
BadgeDollarSign,
|
|
71
88
|
Banknote,
|
|
72
89
|
Layers,
|
|
73
90
|
Music,
|
|
74
91
|
Eye,
|
|
75
92
|
ListMusic,
|
|
93
|
+
ListPlus,
|
|
94
|
+
Map,
|
|
95
|
+
Ban,
|
|
96
|
+
Clock3,
|
|
76
97
|
Disc3,
|
|
77
98
|
UsersRound,
|
|
78
99
|
Mic,
|
|
100
|
+
Inbox,
|
|
101
|
+
Star,
|
|
102
|
+
Zap,
|
|
79
103
|
type LucideIcon,
|
|
80
104
|
} from 'lucide-react'
|
|
81
105
|
import { cn } from '../utils/cn'
|
|
@@ -175,13 +199,14 @@ function TopbarUserMenu({
|
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
export const ICON_MAP: Record<string, LucideIcon> = {
|
|
178
|
-
Home, Users, Settings, CreditCard, Bell, Calendar, Package, BarChart3,
|
|
179
|
-
FileText, Mail, DollarSign, Megaphone, ShoppingCart, Target, Wrench,
|
|
180
|
-
ClipboardList, Briefcase, UserCog, BookOpen, MessageCircle, Globe,
|
|
181
|
-
Percent, Tag, Camera, UtensilsCrossed, Search, MapPin, Handshake,
|
|
182
|
-
Contact, Building2, ChevronDown, Filter, Plus, List, Dog, Cat, PawPrint, Heart, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
183
|
-
ArrowDownCircle, ArrowUpCircle, ArrowLeftRight, Plug, SlidersHorizontal, TableProperties, Landmark, Receipt, TrendingUp, CircleDollarSign, Clock, AlertTriangle, Sparkles, Wallet, Warehouse, Ruler, ArrowUpRight, ArrowDownRight, UserPlus, TreePalm,
|
|
184
|
-
PartyPopper, Radio, CalendarDays, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
202
|
+
Home, Users, Settings, CreditCard, Bell, Calendar, CalendarClock, Package, Activity, BarChart3,
|
|
203
|
+
FileText, Mail, DollarSign, Megaphone, ShoppingCart, ShoppingBag, Target, Wrench,
|
|
204
|
+
ClipboardList, ClipboardCheck, Briefcase, UserCog, BookOpen, MessageCircle, Globe,
|
|
205
|
+
BookOpenCheck, Percent, Tag, Tags, Camera, UtensilsCrossed, Search, MapPin, Handshake,
|
|
206
|
+
Contact, Building2, ChevronDown, Filter, Plus, Shield, List, ListChecks, FolderOpen, Dog, Cat, PawPrint, Heart, LayoutTemplate, LeafyGreen, Apple, Egg, Wheat,
|
|
207
|
+
ArrowDownCircle, ArrowUpCircle, ArrowLeftRight, Plug, SlidersHorizontal, TableProperties, Landmark, Receipt, TrendingUp, CircleDollarSign, Clock, AlertTriangle, Sparkles, Wallet, Warehouse, Ruler, ArrowUpRight, ArrowDownRight, UserPlus, UserX, TreePalm,
|
|
208
|
+
Boxes, Box, FileCheck2, PartyPopper, Radio, CalendarDays, BadgeDollarSign, Banknote, Layers, Music, Eye, ListMusic, Disc3, UsersRound, Mic,
|
|
209
|
+
CalendarCheck2, CalendarX, ListPlus, Map, Ban, Clock3, Inbox, UserCheck, Star, Zap,
|
|
185
210
|
}
|
|
186
211
|
|
|
187
212
|
function getIcon(name: string): LucideIcon {
|
|
@@ -281,15 +306,19 @@ export function Topbar({
|
|
|
281
306
|
{/* Row 1: Logo + Search + Actions */}
|
|
282
307
|
<div className="border-b border-sidebar-border bg-sidebar text-sidebar-foreground">
|
|
283
308
|
<div className="flex h-14 w-full items-center justify-between px-4 md:px-6">
|
|
284
|
-
{/* Left: Mobile menu + Logo
|
|
309
|
+
{/* Left: Mobile menu + Logo — the hamburger is only rendered when an
|
|
310
|
+
onMenuClick handler is supplied. Mobile-first apps with a bottom
|
|
311
|
+
nav omit it so the bottom bar is the primary navigation. */}
|
|
285
312
|
<div className="flex items-center gap-3">
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
{onMenuClick && (
|
|
314
|
+
<button
|
|
315
|
+
onClick={onMenuClick}
|
|
316
|
+
className="inline-flex items-center justify-center rounded-md p-2 text-sidebar-foreground/70 hover:bg-sidebar/30 hover:text-sidebar-foreground md:hidden"
|
|
317
|
+
aria-label="Open menu"
|
|
318
|
+
>
|
|
319
|
+
<Menu className="h-5 w-5" />
|
|
320
|
+
</button>
|
|
321
|
+
)}
|
|
293
322
|
<div className="hidden shrink-0 items-center md:flex">
|
|
294
323
|
{logo ?? <span className="text-lg font-bold">App</span>}
|
|
295
324
|
</div>
|
|
@@ -299,7 +328,7 @@ export function Topbar({
|
|
|
299
328
|
<div className="mx-8 hidden max-w-sm flex-1 md:flex">
|
|
300
329
|
<button
|
|
301
330
|
onClick={() => setCommandPaletteOpen(true)}
|
|
302
|
-
className="relative flex h-8 w-full items-center rounded-md border border-sidebar-border bg-sidebar-accent px-3 pl-8 text-sm text-sidebar-muted shadow-sm transition-colors hover:bg-
|
|
331
|
+
className="relative flex h-8 w-full items-center rounded-md border border-sidebar-border bg-sidebar-accent px-3 pl-8 text-sm text-sidebar-muted shadow-sm transition-colors hover:border-sidebar-foreground/25 hover:bg-sidebar-foreground/10 hover:text-sidebar-foreground"
|
|
303
332
|
>
|
|
304
333
|
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-sidebar-muted" />
|
|
305
334
|
<span className="text-xs">{searchPlaceholder}</span>
|
|
@@ -335,7 +364,7 @@ export function Topbar({
|
|
|
335
364
|
const left = active.offsetLeft - el.offsetWidth / 2 + active.offsetWidth / 2
|
|
336
365
|
el.scrollTo({ left: Math.max(0, left), behavior: 'smooth' })
|
|
337
366
|
}
|
|
338
|
-
}} className="flex items-center gap-0.5 overflow-x-auto scrollbar-
|
|
367
|
+
}} className="flex items-center gap-0.5 overflow-x-auto scrollbar-none">
|
|
339
368
|
{allNav.map((item) => {
|
|
340
369
|
if (item.children && item.children.length > 0) {
|
|
341
370
|
return (
|
package/src/layout/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
AppShell,
|
|
3
|
+
type AppShellProps,
|
|
4
|
+
type AppShellUser,
|
|
5
|
+
type BottomNavItem,
|
|
6
|
+
type BottomNavRouteItem,
|
|
7
|
+
type BottomNavActionItem,
|
|
8
|
+
type MobileHeaderVariant,
|
|
9
|
+
} from './AppShell'
|
|
2
10
|
export { Sidebar, type SidebarProps, type SidebarUser, type NavigationItem, ICON_MAP } from './Sidebar'
|
|
3
11
|
export { Topbar, type TopbarProps } from './Topbar'
|
|
4
12
|
export {
|