@fayz-ai/ui 0.6.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.
Files changed (44) hide show
  1. package/dist/{chunk-7TBDPNLY.cjs → chunk-5QFB6AIQ.cjs} +40 -11
  2. package/dist/chunk-5QFB6AIQ.cjs.map +1 -0
  3. package/dist/{chunk-GE6XUREM.cjs → chunk-DJHS2XRW.cjs} +185 -21
  4. package/dist/chunk-DJHS2XRW.cjs.map +1 -0
  5. package/dist/{chunk-CZ6X6PZT.js → chunk-LO5QZO7F.js} +192 -28
  6. package/dist/chunk-LO5QZO7F.js.map +1 -0
  7. package/dist/{chunk-GFH5HOCO.js → chunk-XLBGB7IW.js} +40 -12
  8. package/dist/chunk-XLBGB7IW.js.map +1 -0
  9. package/dist/dashboard/DashboardCanvas.d.ts +5 -1
  10. package/dist/dashboard/DashboardCanvas.d.ts.map +1 -1
  11. package/dist/dashboard/GoldCard.d.ts +19 -0
  12. package/dist/dashboard/GoldCard.d.ts.map +1 -0
  13. package/dist/dashboard/KpiCard.d.ts.map +1 -1
  14. package/dist/dashboard/index.cjs +28 -24
  15. package/dist/dashboard/index.d.ts +1 -0
  16. package/dist/dashboard/index.d.ts.map +1 -1
  17. package/dist/dashboard/index.js +1 -1
  18. package/dist/index.cjs +46 -42
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -2
  22. package/dist/layout/AppShell.d.ts +27 -6
  23. package/dist/layout/AppShell.d.ts.map +1 -1
  24. package/dist/layout/Sidebar.d.ts.map +1 -1
  25. package/dist/layout/Topbar.d.ts.map +1 -1
  26. package/dist/layout/index.cjs +17 -17
  27. package/dist/layout/index.d.ts +1 -1
  28. package/dist/layout/index.d.ts.map +1 -1
  29. package/dist/layout/index.js +1 -1
  30. package/package.json +3 -6
  31. package/src/dashboard/DashboardCanvas.tsx +13 -2
  32. package/src/dashboard/GoldCard.tsx +38 -0
  33. package/src/dashboard/KpiCard.tsx +4 -2
  34. package/src/dashboard/index.ts +1 -0
  35. package/src/index.ts +6 -0
  36. package/src/layout/AppShell.tsx +240 -20
  37. package/src/layout/ModulePage.tsx +2 -2
  38. package/src/layout/Sidebar.tsx +32 -6
  39. package/src/layout/Topbar.tsx +46 -17
  40. package/src/layout/index.ts +9 -1
  41. package/dist/chunk-7TBDPNLY.cjs.map +0 -1
  42. package/dist/chunk-CZ6X6PZT.js.map +0 -1
  43. package/dist/chunk-GE6XUREM.cjs.map +0 -1
  44. package/dist/chunk-GFH5HOCO.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkGE6XUREM_cjs = require('../chunk-GE6XUREM.cjs');
3
+ var chunkDJHS2XRW_cjs = require('../chunk-DJHS2XRW.cjs');
4
4
  var chunkW4XXXJRW_cjs = require('../chunk-W4XXXJRW.cjs');
5
5
  require('../chunk-VVVZO2LE.cjs');
6
6
 
@@ -8,67 +8,67 @@ require('../chunk-VVVZO2LE.cjs');
8
8
 
9
9
  Object.defineProperty(exports, "AppShell", {
10
10
  enumerable: true,
11
- get: function () { return chunkGE6XUREM_cjs.AppShell; }
11
+ get: function () { return chunkDJHS2XRW_cjs.AppShell; }
12
12
  });
13
13
  Object.defineProperty(exports, "BackStyleProvider", {
14
14
  enumerable: true,
15
- get: function () { return chunkGE6XUREM_cjs.BackStyleProvider; }
15
+ get: function () { return chunkDJHS2XRW_cjs.BackStyleProvider; }
16
16
  });
17
17
  Object.defineProperty(exports, "ICON_MAP", {
18
18
  enumerable: true,
19
- get: function () { return chunkGE6XUREM_cjs.ICON_MAP; }
19
+ get: function () { return chunkDJHS2XRW_cjs.ICON_MAP; }
20
20
  });
21
21
  Object.defineProperty(exports, "ModuleLayoutProvider", {
22
22
  enumerable: true,
23
- get: function () { return chunkGE6XUREM_cjs.ModuleLayoutProvider; }
23
+ get: function () { return chunkDJHS2XRW_cjs.ModuleLayoutProvider; }
24
24
  });
25
25
  Object.defineProperty(exports, "ModulePage", {
26
26
  enumerable: true,
27
- get: function () { return chunkGE6XUREM_cjs.ModulePage; }
27
+ get: function () { return chunkDJHS2XRW_cjs.ModulePage; }
28
28
  });
29
29
  Object.defineProperty(exports, "NavTransitionProvider", {
30
30
  enumerable: true,
31
- get: function () { return chunkGE6XUREM_cjs.NavTransitionProvider; }
31
+ get: function () { return chunkDJHS2XRW_cjs.NavTransitionProvider; }
32
32
  });
33
33
  Object.defineProperty(exports, "PageHeaderActions", {
34
34
  enumerable: true,
35
- get: function () { return chunkGE6XUREM_cjs.PageHeaderActions; }
35
+ get: function () { return chunkDJHS2XRW_cjs.PageHeaderActions; }
36
36
  });
37
37
  Object.defineProperty(exports, "PageTransition", {
38
38
  enumerable: true,
39
- get: function () { return chunkGE6XUREM_cjs.PageTransition; }
39
+ get: function () { return chunkDJHS2XRW_cjs.PageTransition; }
40
40
  });
41
41
  Object.defineProperty(exports, "Sidebar", {
42
42
  enumerable: true,
43
- get: function () { return chunkGE6XUREM_cjs.Sidebar; }
43
+ get: function () { return chunkDJHS2XRW_cjs.Sidebar; }
44
44
  });
45
45
  Object.defineProperty(exports, "SubpageHeader", {
46
46
  enumerable: true,
47
- get: function () { return chunkGE6XUREM_cjs.SubpageHeader; }
47
+ get: function () { return chunkDJHS2XRW_cjs.SubpageHeader; }
48
48
  });
49
49
  Object.defineProperty(exports, "Topbar", {
50
50
  enumerable: true,
51
- get: function () { return chunkGE6XUREM_cjs.Topbar; }
51
+ get: function () { return chunkDJHS2XRW_cjs.Topbar; }
52
52
  });
53
53
  Object.defineProperty(exports, "useBackStyle", {
54
54
  enumerable: true,
55
- get: function () { return chunkGE6XUREM_cjs.useBackStyle; }
55
+ get: function () { return chunkDJHS2XRW_cjs.useBackStyle; }
56
56
  });
57
57
  Object.defineProperty(exports, "useModuleHeaderActionsSlot", {
58
58
  enumerable: true,
59
- get: function () { return chunkGE6XUREM_cjs.useModuleHeaderActionsSlot; }
59
+ get: function () { return chunkDJHS2XRW_cjs.useModuleHeaderActionsSlot; }
60
60
  });
61
61
  Object.defineProperty(exports, "useModuleHeaderSlot", {
62
62
  enumerable: true,
63
- get: function () { return chunkGE6XUREM_cjs.useModuleHeaderSlot; }
63
+ get: function () { return chunkDJHS2XRW_cjs.useModuleHeaderSlot; }
64
64
  });
65
65
  Object.defineProperty(exports, "useModuleLayout", {
66
66
  enumerable: true,
67
- get: function () { return chunkGE6XUREM_cjs.useModuleLayout; }
67
+ get: function () { return chunkDJHS2XRW_cjs.useModuleLayout; }
68
68
  });
69
69
  Object.defineProperty(exports, "useNavTransition", {
70
70
  enumerable: true,
71
- get: function () { return chunkGE6XUREM_cjs.useNavTransition; }
71
+ get: function () { return chunkDJHS2XRW_cjs.useNavTransition; }
72
72
  });
73
73
  Object.defineProperty(exports, "SaveBar", {
74
74
  enumerable: true,
@@ -1,4 +1,4 @@
1
- export { AppShell, type AppShellProps, type AppShellUser } from './AppShell';
1
+ export { AppShell, type AppShellProps, type AppShellUser, type BottomNavItem, type BottomNavRouteItem, type BottomNavActionItem, type MobileHeaderVariant, } from './AppShell';
2
2
  export { Sidebar, type SidebarProps, type SidebarUser, type NavigationItem, ICON_MAP } from './Sidebar';
3
3
  export { Topbar, type TopbarProps } from './Topbar';
4
4
  export { ModulePage, SubpageHeader, ModuleLayoutProvider, useModuleLayout, PageHeaderActions, BackStyleProvider, useBackStyle, type ModulePageProps, type ModuleNavItem, type ModuleNavVariant, type SubpageHeaderProps, type BackButtonStyle, } from './ModulePage';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACvG,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EACL,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAC1G,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layout/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACvG,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EACL,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAA;AAC1G,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,aAAa,GACnB,MAAM,kBAAkB,CAAA"}
@@ -1,4 +1,4 @@
1
- export { AppShell, BackStyleProvider, ICON_MAP, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageTransition, Sidebar, SubpageHeader, Topbar, useBackStyle, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition } from '../chunk-CZ6X6PZT.js';
1
+ export { AppShell, BackStyleProvider, ICON_MAP, ModuleLayoutProvider, ModulePage, NavTransitionProvider, PageHeaderActions, PageTransition, Sidebar, SubpageHeader, Topbar, useBackStyle, useModuleHeaderActionsSlot, useModuleHeaderSlot, useModuleLayout, useNavTransition } from '../chunk-LO5QZO7F.js';
2
2
  export { SaveBar, SaveBarProvider, useBackHandler, useSaveBar } from '../chunk-E526FHZ4.js';
3
3
  import '../chunk-DNYVEOAX.js';
4
4
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayz-ai/ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.6",
4
4
  "description": "Fayz SDK UI — Radix + Tailwind component library with CRUD engine and layout shells",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -81,8 +81,8 @@
81
81
  "sonner": "^1.5.0",
82
82
  "tailwind-merge": "^2.4.0",
83
83
  "zustand": "^4.5.0",
84
- "@fayz-ai/core": "^0.6.0",
85
- "@fayz-ai/auth": "^0.6.0"
84
+ "@fayz-ai/core": "^0.6.6",
85
+ "@fayz-ai/auth": "^0.6.6"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@types/react": "^18.3.0",
@@ -96,9 +96,6 @@
96
96
  "typescript": "^5.5.0"
97
97
  },
98
98
  "license": "MIT",
99
- "publishConfig": {
100
- "access": "public"
101
- },
102
99
  "scripts": {
103
100
  "build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
104
101
  "dev": "tsup --watch",
@@ -90,6 +90,10 @@ export interface DashboardCanvasProps {
90
90
  showHeader?: boolean
91
91
  /** App-level curation of which widgets show, their order and span. */
92
92
  appLayout?: DashboardLayoutConfig
93
+ /** App-level widgets injected into this surface's grid without a plugin — e.g.
94
+ * an "Ask the assistant" CTA. Merged with the registry widgets, then laid out
95
+ * (respects each widget's span/defaultOrder). Filtered to this surface. */
96
+ extraWidgets?: DashboardWidgetDef[]
93
97
  /** Show a shared time-range control (sticky, top-right). Widgets read it via
94
98
  * useDashboardRange(). Pass `true` for defaults or an options object. */
95
99
  range?: boolean | DashboardRangeOptions
@@ -115,11 +119,18 @@ export interface DashboardRangeOptions {
115
119
  }
116
120
 
117
121
  export function DashboardCanvas({
118
- surface, domain, title, subtitle, showHeader = true, appLayout, range,
122
+ surface, domain, title, subtitle, showHeader = true, appLayout, extraWidgets, range,
119
123
  customizable = true, customizeLabel, initialVisibility, onNavigate, className,
120
124
  }: DashboardCanvasProps) {
121
125
  const runtime = usePluginRuntime()
122
- const widgets = getDashboardWidgets(runtime, { surface, domain })
126
+ const registryWidgets = getDashboardWidgets(runtime, { surface, domain })
127
+ const widgets = React.useMemo<DashboardWidgetDef[]>(() => {
128
+ const extra = (extraWidgets ?? []).filter((w) => {
129
+ if (!(w.surfaces ?? ['home', 'plugin-home']).includes(surface)) return false
130
+ return !domain || w.domain === domain
131
+ })
132
+ return extra.length ? [...registryWidgets, ...extra] : registryWidgets
133
+ }, [registryWidgets, extraWidgets, surface, domain])
123
134
  const rangeOpts: DashboardRangeOptions | null = range ? (range === true ? {} : range) : null
124
135
 
125
136
  const forceAll = (initialVisibility ?? (surface === 'plugin-home' ? 'all' : 'default')) === 'all'
@@ -0,0 +1,38 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../utils/cn'
3
+
4
+ export interface GoldCardProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ /**
6
+ * Brand-colored hero variant — `bg-primary text-primary-foreground` with a
7
+ * soft branded shadow. Use for the headline card (e.g. a balance hero). When
8
+ * false (default) it's a plain surface card (`bg-card` + border).
9
+ */
10
+ branded?: boolean
11
+ }
12
+
13
+ /**
14
+ * GoldCard — a generic, reusable rounded card wrapper for dashboard surfaces.
15
+ *
16
+ * The `branded` variant paints the card in the app's primary color so a single
17
+ * hero (balance, headline KPI, CTA) pops against the neutral grid. Both variants
18
+ * are internally full-width so they reflow cleanly inside the responsive
19
+ * DashboardGrid (full-width on mobile, cell-width on desktop).
20
+ */
21
+ export const GoldCard = React.forwardRef<HTMLDivElement, GoldCardProps>(
22
+ ({ branded = false, className, children, ...rest }, ref) => (
23
+ <div
24
+ ref={ref}
25
+ className={cn(
26
+ 'h-full rounded-3xl p-5',
27
+ branded
28
+ ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
29
+ : 'border border-border bg-card text-card-foreground shadow-sm',
30
+ className,
31
+ )}
32
+ {...rest}
33
+ >
34
+ {children}
35
+ </div>
36
+ ),
37
+ )
38
+ GoldCard.displayName = 'GoldCard'
@@ -222,8 +222,10 @@ export function KpiCard(props: KpiCardProps) {
222
222
  }
223
223
 
224
224
  return (
225
- <Card>
226
- <CardContent className="p-4">
225
+ // h-full so cards fill the (stretch) grid cell and every card in a row matches
226
+ // the tallest — labels/subtitles of different lengths no longer misalign them.
227
+ <Card className="h-full">
228
+ <CardContent className="flex h-full flex-col p-4">
227
229
  <div className="mb-2 flex items-center justify-between gap-2">
228
230
  <span className="text-sm text-muted-foreground">{label}</span>
229
231
  <span className="text-muted-foreground">{renderIcon(icon)}</span>
@@ -10,6 +10,7 @@ export { resolveDashboardLayout, type LaidOutWidget } from './layout'
10
10
  export { useDashboardPreferences, type DashboardPreferencesState } from './preferences'
11
11
  export { DashboardCustomizeMenu, type DashboardCustomizeMenuProps } from './DashboardCustomizeMenu'
12
12
  export { DashboardGrid, type DashboardGridItem, type DashboardGridProps } from './DashboardGrid'
13
+ export { GoldCard, type GoldCardProps } from './GoldCard'
13
14
  export {
14
15
  DashboardRangeProvider,
15
16
  DashboardRangeControl,
package/src/index.ts CHANGED
@@ -105,6 +105,10 @@ export {
105
105
  AppShell,
106
106
  type AppShellProps,
107
107
  type AppShellUser,
108
+ type BottomNavItem,
109
+ type BottomNavRouteItem,
110
+ type BottomNavActionItem,
111
+ type MobileHeaderVariant,
108
112
  Sidebar,
109
113
  type SidebarProps,
110
114
  type SidebarUser,
@@ -179,6 +183,8 @@ export {
179
183
  DashboardGrid,
180
184
  type DashboardGridItem,
181
185
  type DashboardGridProps,
186
+ GoldCard,
187
+ type GoldCardProps,
182
188
  DashboardRangeProvider,
183
189
  DashboardRangeControl,
184
190
  useDashboardRange,
@@ -1,5 +1,18 @@
1
1
  import * as React from 'react'
2
- import { Menu } from 'lucide-react'
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?: Array<{ label: string; icon: string; route: string }>
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="flex h-screen overflow-hidden bg-background">
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
- <div className="flex min-h-14 shrink-0 items-center justify-between gap-3 border-b border-border bg-card px-4 py-2">
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
- <button
165
- onClick={() => setMobileMenuOpen(true)}
166
- className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent md:hidden"
167
- aria-label="Open menu"
168
- >
169
- <Menu className="h-5 w-5" />
170
- </button>
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
- <main className={cn('flex-1 overflow-y-auto', frame && 'md:p-2')}>
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
- return (
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
- return (
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
- return (
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-hide">
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-hide">
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