@mohammad-irfan/create-dashboard-kit 1.0.0

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/create-dashboard.js +12 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +223 -0
  6. package/dist/configBuilder.d.ts +8 -0
  7. package/dist/configBuilder.js +573 -0
  8. package/dist/generator.d.ts +9 -0
  9. package/dist/generator.js +223 -0
  10. package/dist/presets.d.ts +8 -0
  11. package/dist/presets.js +72 -0
  12. package/dist/prompts.d.ts +2 -0
  13. package/dist/prompts.js +121 -0
  14. package/dist/templateEngine.d.ts +3 -0
  15. package/dist/templateEngine.js +31 -0
  16. package/dist/types.d.ts +54 -0
  17. package/dist/types.js +1 -0
  18. package/dist/validation.d.ts +16 -0
  19. package/dist/validation.js +73 -0
  20. package/package.json +56 -0
  21. package/templates/source/components/controls/Button.tsx +69 -0
  22. package/templates/source/components/controls/Input.tsx +59 -0
  23. package/templates/source/components/controls/SearchBar.tsx +44 -0
  24. package/templates/source/components/controls/Select.tsx +61 -0
  25. package/templates/source/components/controls/ThemeToggle.tsx +23 -0
  26. package/templates/source/components/feedback/NotificationCenter.tsx +151 -0
  27. package/templates/source/components/feedback/Skeleton.tsx +22 -0
  28. package/templates/source/components/feedback/StatusBadge.tsx +35 -0
  29. package/templates/source/components/feedback/statusBadgeStyles.ts +28 -0
  30. package/templates/source/components/icons/IconSet.tsx +235 -0
  31. package/templates/source/components/layout/AppShell.tsx +70 -0
  32. package/templates/source/components/layout/Header.tsx +90 -0
  33. package/templates/source/components/layout/PageHeader.tsx +27 -0
  34. package/templates/source/components/layout/Sidebar.tsx +202 -0
  35. package/templates/source/components/metrics/MetricRow.tsx +61 -0
  36. package/templates/source/components/metrics/StatCard.tsx +49 -0
  37. package/templates/source/components/navigation/NavItem.tsx +75 -0
  38. package/templates/source/components/navigation/ShortcutPill.tsx +39 -0
  39. package/templates/source/components/surfaces/EmptyState.tsx +36 -0
  40. package/templates/source/components/surfaces/HeroSurface.tsx +64 -0
  41. package/templates/source/components/surfaces/Panel.tsx +43 -0
  42. package/templates/source/components/surfaces/PriorityCard.tsx +75 -0
  43. package/templates/source/components/surfaces/QuietListCard.tsx +146 -0
  44. package/templates/source/design-system/animations.css +25 -0
  45. package/templates/source/design-system/tokens.css +74 -0
  46. package/templates/source/design-system/typography.css +34 -0
  47. package/templates/source/theme/ThemeContext.tsx +82 -0
  48. package/templates/source/theme/useTheme.ts +10 -0
@@ -0,0 +1,235 @@
1
+ import type { SVGProps } from 'react'
2
+
3
+ export type IconProps = SVGProps<SVGSVGElement>
4
+
5
+ export function Icon({ children, ...props }: IconProps) {
6
+ return (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth={1.5}
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ aria-hidden="true"
16
+ {...props}
17
+ >
18
+ {children}
19
+ </svg>
20
+ )
21
+ }
22
+
23
+ export function DashboardIcon(props: IconProps) {
24
+ return (
25
+ <Icon {...props}>
26
+ <path d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25a2.25 2.25 0 0 1-2.25-2.25v-2.25Z" />
27
+ </Icon>
28
+ )
29
+ }
30
+
31
+ export function MenuIcon(props: IconProps) {
32
+ return (
33
+ <Icon {...props}>
34
+ <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
35
+ </Icon>
36
+ )
37
+ }
38
+
39
+ export function CloseIcon(props: IconProps) {
40
+ return (
41
+ <Icon {...props}>
42
+ <path d="M6 18 18 6M6 6l12 12" />
43
+ </Icon>
44
+ )
45
+ }
46
+
47
+ export function CollapseIcon(props: IconProps) {
48
+ return (
49
+ <Icon {...props}>
50
+ <path d="M18.75 19.5 11.25 12l7.5-7.5m-6 15L5.25 12l7.5-7.5" />
51
+ </Icon>
52
+ )
53
+ }
54
+
55
+ export function PlusIcon(props: IconProps) {
56
+ return (
57
+ <Icon {...props}>
58
+ <path d="M12 4.5v15m7.5-7.5h-15" />
59
+ </Icon>
60
+ )
61
+ }
62
+
63
+ export function ExternalLinkIcon(props: IconProps) {
64
+ return (
65
+ <Icon {...props}>
66
+ <path d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
67
+ </Icon>
68
+ )
69
+ }
70
+
71
+ export function SignOutIcon(props: IconProps) {
72
+ return (
73
+ <Icon {...props}>
74
+ <path d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l3 3m0 0-3 3m3-3H2.25" />
75
+ </Icon>
76
+ )
77
+ }
78
+
79
+ export function BellIcon(props: IconProps) {
80
+ return (
81
+ <Icon {...props}>
82
+ <path d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
83
+ </Icon>
84
+ )
85
+ }
86
+
87
+ export function CheckIcon(props: IconProps) {
88
+ return (
89
+ <Icon {...props}>
90
+ <path d="m4.5 12.75 6 6 9-13.5" />
91
+ </Icon>
92
+ )
93
+ }
94
+
95
+ export function SunIcon(props: IconProps) {
96
+ return (
97
+ <Icon {...props}>
98
+ <path d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" />
99
+ </Icon>
100
+ )
101
+ }
102
+
103
+ export function MoonIcon(props: IconProps) {
104
+ return (
105
+ <Icon {...props}>
106
+ <path d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
107
+ </Icon>
108
+ )
109
+ }
110
+
111
+ export function SparklesIcon(props: IconProps) {
112
+ return (
113
+ <Icon {...props}>
114
+ <path d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
115
+ </Icon>
116
+ )
117
+ }
118
+
119
+ export function ArrowRightIcon(props: IconProps) {
120
+ return (
121
+ <Icon {...props}>
122
+ <path d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
123
+ </Icon>
124
+ )
125
+ }
126
+
127
+ export function SearchIcon(props: IconProps) {
128
+ return (
129
+ <Icon {...props}>
130
+ <path d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
131
+ </Icon>
132
+ )
133
+ }
134
+
135
+ export function FilterIcon(props: IconProps) {
136
+ return (
137
+ <Icon {...props}>
138
+ <path d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75" />
139
+ </Icon>
140
+ )
141
+ }
142
+
143
+ export function FolderIcon(props: IconProps) {
144
+ return (
145
+ <Icon {...props}>
146
+ <path d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" />
147
+ </Icon>
148
+ )
149
+ }
150
+
151
+ export function UsersIcon(props: IconProps) {
152
+ return (
153
+ <Icon {...props}>
154
+ <path d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
155
+ </Icon>
156
+ )
157
+ }
158
+
159
+ export function CoffeeIcon(props: IconProps) {
160
+ return (
161
+ <Icon {...props}>
162
+ <path d="M18 8h1a4 4 0 0 1 0 8h-1M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8ZM6 1v3M10 1v3M14 1v3" />
163
+ </Icon>
164
+ )
165
+ }
166
+
167
+ export function DollarIcon(props: IconProps) {
168
+ return (
169
+ <Icon {...props}>
170
+ <path d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
171
+ </Icon>
172
+ )
173
+ }
174
+
175
+ export function ClipboardIcon(props: IconProps) {
176
+ return (
177
+ <Icon {...props}>
178
+ <path d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375-3-3m0 0 3-3m-3 3h10.5" />
179
+ </Icon>
180
+ )
181
+ }
182
+
183
+ export function ClockIcon(props: IconProps) {
184
+ return (
185
+ <Icon {...props}>
186
+ <path d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
187
+ </Icon>
188
+ )
189
+ }
190
+
191
+ export function SettingsIcon(props: IconProps) {
192
+ return (
193
+ <Icon {...props}>
194
+ <path d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.6 6.6 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
195
+ <path d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
196
+ </Icon>
197
+ )
198
+ }
199
+
200
+ export function renderNamedIcon(name: string, props?: IconProps) {
201
+ switch (name) {
202
+ case 'dashboard':
203
+ return <DashboardIcon {...props} />
204
+ case 'folder':
205
+ return <FolderIcon {...props} />
206
+ case 'check':
207
+ return <CheckIcon {...props} />
208
+ case 'users':
209
+ return <UsersIcon {...props} />
210
+ case 'coffee':
211
+ return <CoffeeIcon {...props} />
212
+ case 'dollar':
213
+ return <DollarIcon {...props} />
214
+ case 'clipboard':
215
+ return <ClipboardIcon {...props} />
216
+ case 'clock':
217
+ return <ClockIcon {...props} />
218
+ case 'settings':
219
+ return <SettingsIcon {...props} />
220
+ case 'sparkles':
221
+ return <SparklesIcon {...props} />
222
+ case 'bell':
223
+ return <BellIcon {...props} />
224
+ case 'search':
225
+ return <SearchIcon {...props} />
226
+ case 'filter':
227
+ return <FilterIcon {...props} />
228
+ default:
229
+ return (
230
+ <span className="h-5 w-5 shrink-0 flex items-center justify-center font-bold text-xs">
231
+ {name}
232
+ </span>
233
+ )
234
+ }
235
+ }
@@ -0,0 +1,70 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { Header } from './Header.tsx'
3
+ import { Sidebar } from './Sidebar.tsx'
4
+ import type { DashboardConfig } from '../../config/dashboard.config.ts'
5
+ import { getCanvasRadiusClass } from '../../config/dashboard.config.ts'
6
+
7
+ export interface AppShellProps {
8
+ config: DashboardConfig
9
+ currentPath?: string
10
+ pageTitle?: string
11
+ onNavigate?: (to: string) => void
12
+ headerActions?: ReactNode
13
+ footer?: ReactNode
14
+ children: ReactNode
15
+ className?: string
16
+ }
17
+
18
+ export function AppShell({
19
+ config,
20
+ currentPath = '/dashboard',
21
+ pageTitle,
22
+ onNavigate,
23
+ headerActions,
24
+ footer,
25
+ children,
26
+ className = '',
27
+ }: AppShellProps) {
28
+ const [collapsed, setCollapsed] = useState(false)
29
+ const [mobileOpen, setMobileOpen] = useState(false)
30
+
31
+ const canvasRadiusClass = getCanvasRadiusClass(config.shapes.canvasRadius)
32
+
33
+ return (
34
+ <div className={`flex h-dvh flex-col overflow-hidden bg-white dark:bg-[#111315] text-foreground ${className}`}>
35
+ {/* PERSISTENT HEADER */}
36
+ <Header
37
+ config={config}
38
+ pageTitle={pageTitle}
39
+ onOpenSidebar={() => setMobileOpen(true)}
40
+ trailingActions={headerActions}
41
+ />
42
+
43
+ {/* BODY SHELL */}
44
+ <div className="flex flex-1 min-h-0 overflow-hidden">
45
+ {/* SIDEBAR RAIL & DRAWER */}
46
+ <Sidebar
47
+ config={config}
48
+ collapsed={collapsed}
49
+ mobileOpen={mobileOpen}
50
+ currentPath={currentPath}
51
+ onToggleCollapse={() => setCollapsed((prev) => !prev)}
52
+ onCloseMobile={() => setMobileOpen(false)}
53
+ onNavigate={onNavigate}
54
+ />
55
+
56
+ {/* SCROLLABLE MAIN CONTENT CANVAS WITH SIGNATURE SCOOPED CORNER */}
57
+ <main
58
+ className={`flex-1 overflow-y-auto bg-[var(--canvas-background,#eef3fb)] ${canvasRadiusClass} transition-all duration-200`}
59
+ >
60
+ <div className="min-h-full py-6 sm:py-8 lg:py-10">
61
+ <div className="mx-auto max-w-[1360px] px-4 sm:px-6 lg:px-8">
62
+ {children}
63
+ </div>
64
+ </div>
65
+ {footer}
66
+ </main>
67
+ </div>
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,90 @@
1
+ import type { ReactNode } from 'react'
2
+ import { MenuIcon } from '../icons/IconSet.tsx'
3
+ import { ThemeToggle } from '../controls/ThemeToggle.tsx'
4
+ import type { DashboardConfig } from '../../config/dashboard.config.ts'
5
+
6
+ export interface HeaderProps {
7
+ config: DashboardConfig
8
+ pageTitle?: string
9
+ onOpenSidebar?: () => void
10
+ trailingActions?: ReactNode
11
+ className?: string
12
+ }
13
+
14
+ export function Header({
15
+ config,
16
+ pageTitle,
17
+ onOpenSidebar,
18
+ trailingActions,
19
+ className = '',
20
+ }: HeaderProps) {
21
+ const { branding, userMenu } = config
22
+ const LogoIcon = branding.logoIcon
23
+
24
+ return (
25
+ <header className={`shrink-0 bg-white dark:bg-[#111315] border-b border-border/20 ${className}`}>
26
+ <div className="flex h-16 items-center gap-3 px-4 sm:px-6 lg:px-8">
27
+ {onOpenSidebar ? (
28
+ <button
29
+ type="button"
30
+ onClick={onOpenSidebar}
31
+ aria-label="Open navigation drawer"
32
+ className="inline-flex items-center justify-center rounded-xl p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring lg:hidden"
33
+ >
34
+ <MenuIcon className="h-5 w-5" />
35
+ </button>
36
+ ) : null}
37
+
38
+ <a
39
+ href={branding.homeRoute}
40
+ aria-label={`${branding.name} home`}
41
+ className="flex items-center gap-2.5 text-base font-bold tracking-tight text-foreground transition-opacity hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xl"
42
+ >
43
+ {branding.logoUrl ? (
44
+ <img
45
+ src={branding.logoUrl}
46
+ alt=""
47
+ className="h-8 w-8 shrink-0 rounded-xl object-cover shadow-xs"
48
+ />
49
+ ) : LogoIcon ? (
50
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary shadow-xs">
51
+ <LogoIcon className="h-5 w-5" />
52
+ </span>
53
+ ) : (
54
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-primary text-white font-bold text-sm shadow-xs">
55
+ {branding.name.charAt(0)}
56
+ </span>
57
+ )}
58
+ <span className="text-base font-bold tracking-tight text-foreground">
59
+ {branding.name}
60
+ </span>
61
+ </a>
62
+
63
+ {pageTitle && pageTitle !== branding.name ? (
64
+ <>
65
+ <span className="hidden h-4 w-px bg-border/60 lg:block ml-1" aria-hidden="true" />
66
+ <h1 className="hidden text-sm font-medium tracking-normal text-muted-foreground lg:block">
67
+ {pageTitle}
68
+ </h1>
69
+ </>
70
+ ) : null}
71
+
72
+ <div className="ml-auto flex items-center gap-2">
73
+ {trailingActions}
74
+ <ThemeToggle />
75
+
76
+ {userMenu ? (
77
+ <a
78
+ href={userMenu.profileRoute || '/settings'}
79
+ title={userMenu.name}
80
+ aria-label={`User profile: ${userMenu.name}`}
81
+ className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-xs font-semibold text-primary ring-1 ring-inset ring-primary/20 transition-transform hover:scale-105 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
82
+ >
83
+ {userMenu.initials || userMenu.name.slice(0, 2).toUpperCase()}
84
+ </a>
85
+ ) : null}
86
+ </div>
87
+ </div>
88
+ </header>
89
+ )
90
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export interface PageHeaderProps {
4
+ title: string
5
+ description?: string
6
+ actions?: ReactNode
7
+ className?: string
8
+ }
9
+
10
+ export function PageHeader({
11
+ title,
12
+ description,
13
+ actions,
14
+ className = '',
15
+ }: PageHeaderProps) {
16
+ return (
17
+ <header className={`mb-6 flex flex-wrap items-start justify-between gap-4 ${className}`}>
18
+ <div>
19
+ <h1 className="text-2xl font-bold tracking-tight text-foreground">{title}</h1>
20
+ {description ? (
21
+ <p className="mt-1 text-sm text-muted-foreground">{description}</p>
22
+ ) : null}
23
+ </div>
24
+ {actions ? <div className="flex items-center gap-3">{actions}</div> : null}
25
+ </header>
26
+ )
27
+ }
@@ -0,0 +1,202 @@
1
+ import type { ReactNode } from 'react'
2
+ import { CloseIcon, CollapseIcon, SignOutIcon } from '../icons/IconSet.tsx'
3
+ import { NavItem } from '../navigation/NavItem.tsx'
4
+ import type { DashboardConfig } from '../../config/dashboard.config.ts'
5
+
6
+ export interface SidebarProps {
7
+ config: DashboardConfig
8
+ collapsed: boolean
9
+ mobileOpen: boolean
10
+ currentPath?: string
11
+ onToggleCollapse: () => void
12
+ onCloseMobile: () => void
13
+ onNavigate?: (to: string) => void
14
+ footerSlot?: ReactNode
15
+ className?: string
16
+ }
17
+
18
+ export function Sidebar({
19
+ config,
20
+ collapsed,
21
+ mobileOpen,
22
+ currentPath = '/dashboard',
23
+ onToggleCollapse,
24
+ onCloseMobile,
25
+ onNavigate,
26
+ footerSlot,
27
+ className = '',
28
+ }: SidebarProps) {
29
+ const { branding, navigation, userMenu } = config
30
+ const LogoIcon = branding.logoIcon
31
+
32
+ const renderNavList = (isMobile = false) => (
33
+ <div className="flex h-full flex-col bg-white dark:bg-[#111315] text-foreground select-none">
34
+ {/* MOBILE DRAWER HEADER */}
35
+ {isMobile ? (
36
+ <div className="flex h-16 shrink-0 items-center justify-between px-4 border-b border-border/30">
37
+ <a
38
+ href={branding.homeRoute}
39
+ onClick={() => onCloseMobile()}
40
+ className="flex items-center gap-2.5 text-base font-bold tracking-tight text-foreground"
41
+ >
42
+ {branding.logoUrl ? (
43
+ <img src={branding.logoUrl} alt="" className="h-8 w-8 rounded-xl object-cover" />
44
+ ) : LogoIcon ? (
45
+ <span className="flex h-8 w-8 items-center justify-center rounded-xl bg-primary/10 text-primary">
46
+ <LogoIcon className="h-5 w-5" />
47
+ </span>
48
+ ) : (
49
+ <span className="flex h-8 w-8 items-center justify-center rounded-xl bg-primary text-white font-bold text-sm">
50
+ {branding.name.charAt(0)}
51
+ </span>
52
+ )}
53
+ <span>{branding.name}</span>
54
+ </a>
55
+ <button
56
+ type="button"
57
+ onClick={onCloseMobile}
58
+ aria-label="Close navigation"
59
+ className="inline-flex items-center justify-center rounded-xl p-2 text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
60
+ >
61
+ <CloseIcon className="h-5 w-5" />
62
+ </button>
63
+ </div>
64
+ ) : (
65
+ /* DESKTOP COLLAPSE BUTTON */
66
+ <div
67
+ className={`shrink-0 pt-3 pb-2 px-3.5 ${
68
+ collapsed ? 'flex justify-center' : 'flex justify-start'
69
+ }`}
70
+ >
71
+ <button
72
+ type="button"
73
+ onClick={onToggleCollapse}
74
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
75
+ title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
76
+ className="inline-flex h-9 w-9 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
77
+ >
78
+ <CollapseIcon
79
+ className={`h-4 w-4 shrink-0 transition-transform duration-200 ${
80
+ collapsed ? 'rotate-180' : ''
81
+ }`}
82
+ />
83
+ </button>
84
+ </div>
85
+ )}
86
+
87
+ {/* NAVIGATION ITEMS */}
88
+ <nav aria-label="Primary" className="flex-1 space-y-1.5 overflow-y-auto px-2 py-4">
89
+ {!collapsed && !isMobile ? (
90
+ <p className="px-4 pb-2 text-[11px] font-bold uppercase tracking-wider text-muted-foreground/70">
91
+ Navigation
92
+ </p>
93
+ ) : null}
94
+
95
+ {navigation.map((item) => {
96
+ const active = currentPath === item.to || (item.end && currentPath.startsWith(item.to))
97
+ return (
98
+ <NavItem
99
+ key={item.id}
100
+ label={item.label}
101
+ to={item.to}
102
+ icon={item.icon}
103
+ badge={item.badge}
104
+ active={active}
105
+ collapsed={!isMobile && collapsed}
106
+ onClick={() => {
107
+ onNavigate?.(item.to)
108
+ if (isMobile) onCloseMobile()
109
+ }}
110
+ />
111
+ )
112
+ })}
113
+ </nav>
114
+
115
+ {/* USER PROFILE FOOTER */}
116
+ {footerSlot || (
117
+ <div className="border-t border-border/30 p-3">
118
+ {collapsed && !isMobile ? (
119
+ <div className="flex flex-col items-center gap-2">
120
+ <a
121
+ href={userMenu.profileRoute || '/settings'}
122
+ title={userMenu.name}
123
+ aria-label={userMenu.name}
124
+ className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-[13px] font-semibold text-primary ring-1 ring-inset ring-primary/20"
125
+ >
126
+ {userMenu.initials || userMenu.name.slice(0, 2).toUpperCase()}
127
+ </a>
128
+ {userMenu.onLogout ? (
129
+ <button
130
+ type="button"
131
+ onClick={userMenu.onLogout}
132
+ aria-label="Log out"
133
+ title="Log out"
134
+ className="inline-flex items-center justify-center rounded-lg p-2 text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none"
135
+ >
136
+ <SignOutIcon className="h-5 w-5" />
137
+ </button>
138
+ ) : null}
139
+ </div>
140
+ ) : (
141
+ <div className="flex items-center justify-between gap-3 px-2 py-1">
142
+ <div className="flex items-center gap-3 min-w-0">
143
+ <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary/10 text-[13px] font-semibold text-primary ring-1 ring-inset ring-primary/20">
144
+ {userMenu.initials || userMenu.name.slice(0, 2).toUpperCase()}
145
+ </span>
146
+ <div className="min-w-0">
147
+ <p className="truncate text-xs font-semibold text-foreground">{userMenu.name}</p>
148
+ {userMenu.email ? (
149
+ <p className="truncate text-[11px] text-muted-foreground">{userMenu.email}</p>
150
+ ) : null}
151
+ </div>
152
+ </div>
153
+ {userMenu.onLogout ? (
154
+ <button
155
+ type="button"
156
+ onClick={userMenu.onLogout}
157
+ aria-label="Log out"
158
+ className="inline-flex items-center justify-center rounded-lg p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground"
159
+ >
160
+ <SignOutIcon className="h-4 w-4" />
161
+ </button>
162
+ ) : null}
163
+ </div>
164
+ )}
165
+ </div>
166
+ )}
167
+ </div>
168
+ )
169
+
170
+ return (
171
+ <>
172
+ {/* DESKTOP IN-FLOW RAIL */}
173
+ <aside
174
+ className={`hidden shrink-0 transition-[width] duration-200 ease-in-out lg:block ${
175
+ collapsed ? 'w-16' : 'w-64'
176
+ } ${className}`}
177
+ >
178
+ {renderNavList(false)}
179
+ </aside>
180
+
181
+ {/* MOBILE BACKDROP */}
182
+ <div
183
+ aria-hidden="true"
184
+ onClick={onCloseMobile}
185
+ className={`fixed inset-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200 lg:hidden ${
186
+ mobileOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
187
+ }`}
188
+ />
189
+
190
+ {/* MOBILE OFF-CANVAS DRAWER */}
191
+ <aside
192
+ aria-label="Navigation drawer"
193
+ aria-hidden={!mobileOpen}
194
+ className={`fixed inset-y-0 left-0 z-50 w-64 shadow-2xl transition-transform duration-200 ease-in-out lg:hidden ${
195
+ mobileOpen ? 'translate-x-0' : '-translate-x-full pointer-events-none'
196
+ }`}
197
+ >
198
+ {renderNavList(true)}
199
+ </aside>
200
+ </>
201
+ )
202
+ }
@@ -0,0 +1,61 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export interface MetricItem {
4
+ id: string
5
+ label: string
6
+ value: ReactNode
7
+ to?: string
8
+ onClick?: () => void
9
+ }
10
+
11
+ export interface MetricRowProps {
12
+ metrics: MetricItem[]
13
+ className?: string
14
+ }
15
+
16
+ export function MetricRow({ metrics, className = '' }: MetricRowProps) {
17
+ return (
18
+ <section aria-label="Summary Metrics" className={`pt-2 ${className}`}>
19
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
20
+ {metrics.map((item) => {
21
+ const content = (
22
+ <div className="rounded-2xl border border-border/40 bg-surface dark:bg-surface-elevated/40 px-4 py-3 shadow-2xs hover:border-primary/40 transition-colors group">
23
+ <p className="text-xl font-bold tabular-nums text-foreground group-hover:text-primary transition-colors">
24
+ {item.value}
25
+ </p>
26
+ <p className="text-xs text-muted-foreground truncate">{item.label}</p>
27
+ </div>
28
+ )
29
+
30
+ if (item.to) {
31
+ return (
32
+ <a
33
+ key={item.id}
34
+ href={item.to}
35
+ onClick={(e) => {
36
+ if (item.onClick) {
37
+ e.preventDefault()
38
+ item.onClick()
39
+ }
40
+ }}
41
+ className="block focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-2xl"
42
+ >
43
+ {content}
44
+ </a>
45
+ )
46
+ }
47
+
48
+ return (
49
+ <div
50
+ key={item.id}
51
+ onClick={item.onClick}
52
+ className={item.onClick ? 'cursor-pointer' : undefined}
53
+ >
54
+ {content}
55
+ </div>
56
+ )
57
+ })}
58
+ </div>
59
+ </section>
60
+ )
61
+ }