@etherealui/react 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/index.d.mts +583 -0
- package/dist/index.d.ts +583 -0
- package/dist/index.js +5189 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5189 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +83 -3
- package/src/blocks/app-shell.tsx +441 -0
- package/src/blocks/billing-card.tsx +2 -0
- package/src/blocks/data-table.tsx +479 -0
- package/src/blocks/filter-bar.tsx +134 -0
- package/src/blocks/header-navbar.tsx +7 -0
- package/src/blocks/metric-grid.tsx +5 -0
- package/src/blocks/profile-form.tsx +2 -0
- package/src/blocks/security-settings.tsx +2 -0
- package/src/blocks/settings-blocks.tsx +406 -0
- package/src/blocks/sidebar.tsx +6 -0
- package/src/blocks/stat-card.tsx +7 -0
- package/src/blocks/stat-cards.tsx +212 -0
- package/src/blocks/team-members.tsx +5 -0
- package/src/index.ts +114 -0
- package/src/primitives/accordion.tsx +61 -0
- package/src/primitives/announcement.tsx +33 -0
- package/src/primitives/avatar.tsx +156 -0
- package/src/primitives/badge.tsx +108 -0
- package/src/primitives/breadcrumb.tsx +45 -0
- package/src/primitives/button-group.tsx +79 -0
- package/src/primitives/button.tsx +165 -0
- package/src/primitives/carousel.tsx +84 -0
- package/src/primitives/chart.tsx +831 -0
- package/src/primitives/checkbox.tsx +26 -0
- package/src/primitives/chip.tsx +37 -0
- package/src/primitives/close-button.tsx +29 -0
- package/src/primitives/color-picker.tsx +62 -0
- package/src/primitives/combobox.tsx +123 -0
- package/src/primitives/command-palette.tsx +216 -0
- package/src/primitives/context-menu.tsx +84 -0
- package/src/primitives/date-calendar.tsx +76 -0
- package/src/primitives/date-picker.tsx +400 -0
- package/src/primitives/dialog.tsx +279 -0
- package/src/primitives/divider.tsx +31 -0
- package/src/primitives/drawer.tsx +95 -0
- package/src/primitives/dropdown-menu.tsx +164 -0
- package/src/primitives/file-upload.tsx +123 -0
- package/src/primitives/icon-button.tsx +23 -0
- package/src/primitives/input.tsx +124 -0
- package/src/primitives/modal.tsx +26 -0
- package/src/primitives/pagination.tsx +58 -0
- package/src/primitives/popover.tsx +28 -0
- package/src/primitives/progress.tsx +145 -0
- package/src/primitives/radio-group.tsx +63 -0
- package/src/primitives/select.tsx +207 -0
- package/src/primitives/skeleton.tsx +61 -0
- package/src/primitives/slider.tsx +44 -0
- package/src/primitives/switch.tsx +146 -0
- package/src/primitives/table.tsx +47 -0
- package/src/primitives/tabs.tsx +86 -0
- package/src/primitives/textarea.tsx +40 -0
- package/src/primitives/toast.tsx +71 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/styles/globals.css +49 -0
- package/src/templates/control-tower.tsx +41 -0
- package/src/templates/executive-analytics.tsx +272 -0
- package/src/templates/workspace-settings.tsx +179 -0
- package/src/utils/cn.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etherealui/react",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Free, Open-Source, Modern, Dark-First Enterprise Dashboard Blocks & Component System for React",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Saeful Rohman <saefulrohmandevelopers@gmail.com> (https://saefulrohman.dev)",
|
|
7
|
+
"homepage": "https://ethereal-ui.saefulrohman.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/XenchinRyu7/Ethereal-UI.git",
|
|
11
|
+
"directory": "packages/ui"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/XenchinRyu7/Ethereal-UI/issues"
|
|
15
|
+
},
|
|
16
|
+
"funding": {
|
|
17
|
+
"type": "github",
|
|
18
|
+
"url": "https://github.com/sponsors/XenchinRyu7"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"react",
|
|
25
|
+
"ui",
|
|
26
|
+
"components",
|
|
27
|
+
"dashboard-blocks",
|
|
28
|
+
"tailwind-css",
|
|
29
|
+
"tailwindcss-v4",
|
|
30
|
+
"radix-ui",
|
|
31
|
+
"design-system",
|
|
32
|
+
"enterprise-ui",
|
|
33
|
+
"ethereal-ui"
|
|
34
|
+
],
|
|
35
|
+
"main": "./dist/index.js",
|
|
36
|
+
"module": "./dist/index.mjs",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"sideEffects": [
|
|
39
|
+
"**/*.css"
|
|
40
|
+
],
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"import": "./dist/index.mjs",
|
|
45
|
+
"require": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./styles.css": "./src/styles/globals.css"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"src"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@radix-ui/react-checkbox": "^1.1.4",
|
|
55
|
+
"@radix-ui/react-dialog": "^1.1.6",
|
|
56
|
+
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
|
57
|
+
"@radix-ui/react-popover": "^1.1.6",
|
|
58
|
+
"@radix-ui/react-select": "^2.1.6",
|
|
59
|
+
"@radix-ui/react-slot": "^1.1.2",
|
|
60
|
+
"@radix-ui/react-switch": "^1.1.3",
|
|
61
|
+
"@radix-ui/react-tabs": "^1.1.3",
|
|
62
|
+
"@radix-ui/react-tooltip": "^1.1.8",
|
|
63
|
+
"class-variance-authority": "^0.7.1",
|
|
64
|
+
"clsx": "^2.1.1",
|
|
65
|
+
"lucide-react": "^0.475.0",
|
|
66
|
+
"tailwind-merge": "^3.0.1"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
70
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/react": "^19.0.8",
|
|
74
|
+
"@types/react-dom": "^19.0.3",
|
|
75
|
+
"react": "^19.0.0",
|
|
76
|
+
"react-dom": "^19.0.0",
|
|
77
|
+
"tsup": "^8.3.6",
|
|
78
|
+
"typescript": "^5.7.3"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"build": "tsup",
|
|
82
|
+
"dev": "tsup --watch",
|
|
83
|
+
"lint": "tsc --noEmit",
|
|
84
|
+
"clean": "rimraf dist"
|
|
85
|
+
}
|
|
6
86
|
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import {
|
|
3
|
+
LayoutDashboard,
|
|
4
|
+
Users,
|
|
5
|
+
Settings,
|
|
6
|
+
Bell,
|
|
7
|
+
Search,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
ChevronDown,
|
|
10
|
+
PanelLeftClose,
|
|
11
|
+
PanelLeftOpen,
|
|
12
|
+
Layers,
|
|
13
|
+
CreditCard,
|
|
14
|
+
FolderKanban,
|
|
15
|
+
BarChart3,
|
|
16
|
+
} from "lucide-react"
|
|
17
|
+
import { cn } from "../utils/cn"
|
|
18
|
+
import { Avatar } from "../primitives/avatar"
|
|
19
|
+
import { Badge } from "../primitives/badge"
|
|
20
|
+
import { Button } from "../primitives/button"
|
|
21
|
+
import { CommandPalette } from "../primitives/command-palette"
|
|
22
|
+
|
|
23
|
+
export interface SidebarNavItem {
|
|
24
|
+
id: string
|
|
25
|
+
label: string
|
|
26
|
+
icon?: React.ReactNode
|
|
27
|
+
badge?: string
|
|
28
|
+
active?: boolean
|
|
29
|
+
children?: Array<{ id: string; label: string; active?: boolean }>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultNavItems: SidebarNavItem[] = [
|
|
33
|
+
{
|
|
34
|
+
id: "overview",
|
|
35
|
+
label: "Overview",
|
|
36
|
+
icon: <LayoutDashboard className="h-4 w-4" />,
|
|
37
|
+
active: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "analytics",
|
|
41
|
+
label: "Analytics",
|
|
42
|
+
icon: <BarChart3 className="h-4 w-4" />,
|
|
43
|
+
children: [
|
|
44
|
+
{ id: "revenue", label: "Revenue & MRR", active: true },
|
|
45
|
+
{ id: "retention", label: "Cohort Retention" },
|
|
46
|
+
{ id: "funnels", label: "Conversion Funnels" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "projects",
|
|
51
|
+
label: "Projects",
|
|
52
|
+
icon: <FolderKanban className="h-4 w-4" />,
|
|
53
|
+
badge: "12",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "customers",
|
|
57
|
+
label: "Customers",
|
|
58
|
+
icon: <Users className="h-4 w-4" />,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "billing",
|
|
62
|
+
label: "Billing",
|
|
63
|
+
icon: <CreditCard className="h-4 w-4" />,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "settings",
|
|
67
|
+
label: "Settings",
|
|
68
|
+
icon: <Settings className="h-4 w-4" />,
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
export interface SidebarShellProps {
|
|
73
|
+
items?: SidebarNavItem[]
|
|
74
|
+
activeId?: string
|
|
75
|
+
onSelect?: (id: string) => void
|
|
76
|
+
defaultCollapsed?: boolean
|
|
77
|
+
workspaceName?: string
|
|
78
|
+
workspaceTier?: string
|
|
79
|
+
className?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const SidebarShell: React.FC<SidebarShellProps> = ({
|
|
83
|
+
items = defaultNavItems,
|
|
84
|
+
activeId,
|
|
85
|
+
onSelect,
|
|
86
|
+
defaultCollapsed = false,
|
|
87
|
+
workspaceName = "Acme Corp",
|
|
88
|
+
workspaceTier = "Pro",
|
|
89
|
+
className,
|
|
90
|
+
}) => {
|
|
91
|
+
const [collapsed, setCollapsed] = React.useState(defaultCollapsed)
|
|
92
|
+
const [selectedId, setSelectedId] = React.useState(activeId || "overview")
|
|
93
|
+
const [openGroups, setOpenGroups] = React.useState<Record<string, boolean>>({
|
|
94
|
+
analytics: true,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const currentId = activeId !== undefined ? activeId : selectedId
|
|
98
|
+
|
|
99
|
+
const handleSelect = (id: string) => {
|
|
100
|
+
setSelectedId(id)
|
|
101
|
+
onSelect?.(id)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<aside
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex flex-col border-r border-zinc-200 bg-zinc-50/70 text-zinc-700 transition-all duration-200 select-none shrink-0 dark:border-zinc-800 dark:bg-zinc-900/60 dark:text-zinc-200",
|
|
108
|
+
collapsed ? "w-16" : "w-60",
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
{/* Workspace Header */}
|
|
113
|
+
<div className="flex h-14 items-center justify-between border-b border-zinc-200 px-3.5 dark:border-zinc-800">
|
|
114
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
115
|
+
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-blue-600 text-white">
|
|
116
|
+
<Layers className="h-4 w-4" />
|
|
117
|
+
</div>
|
|
118
|
+
{!collapsed && (
|
|
119
|
+
<div className="min-w-0">
|
|
120
|
+
<div className="flex items-center gap-1.5">
|
|
121
|
+
<p className="truncate text-xs font-semibold text-zinc-900 dark:text-zinc-100">
|
|
122
|
+
{workspaceName}
|
|
123
|
+
</p>
|
|
124
|
+
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-700 dark:bg-blue-500/15 dark:text-blue-300">
|
|
125
|
+
{workspaceTier}
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
134
|
+
aria-label="Toggle sidebar"
|
|
135
|
+
className="rounded-lg p-1.5 text-zinc-400 hover:bg-zinc-200/60 hover:text-zinc-700 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
|
|
136
|
+
>
|
|
137
|
+
{collapsed ? (
|
|
138
|
+
<PanelLeftOpen className="h-4 w-4" />
|
|
139
|
+
) : (
|
|
140
|
+
<PanelLeftClose className="h-4 w-4" />
|
|
141
|
+
)}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Navigation List */}
|
|
146
|
+
<div className="flex-1 overflow-y-auto p-2.5 space-y-1">
|
|
147
|
+
{!collapsed && (
|
|
148
|
+
<div className="px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-zinc-400">
|
|
149
|
+
Workspace
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
{items.map((item) => {
|
|
153
|
+
const isActive = currentId === item.id
|
|
154
|
+
const hasChildren = Boolean(item.children && item.children.length > 0)
|
|
155
|
+
const isOpen = openGroups[item.id]
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div key={item.id} className="space-y-0.5">
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={() => {
|
|
162
|
+
handleSelect(item.id)
|
|
163
|
+
if (hasChildren) {
|
|
164
|
+
setOpenGroups((prev) => ({
|
|
165
|
+
...prev,
|
|
166
|
+
[item.id]: !prev[item.id],
|
|
167
|
+
}))
|
|
168
|
+
}
|
|
169
|
+
}}
|
|
170
|
+
className={cn(
|
|
171
|
+
"flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-xs font-medium transition-colors cursor-pointer",
|
|
172
|
+
isActive
|
|
173
|
+
? "bg-zinc-200/75 text-zinc-900 dark:bg-zinc-800 dark:text-white"
|
|
174
|
+
: "text-zinc-600 hover:bg-zinc-200/40 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-200"
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
<span className="flex items-center gap-2.5 min-w-0">
|
|
178
|
+
<span
|
|
179
|
+
className={cn(
|
|
180
|
+
"shrink-0",
|
|
181
|
+
isActive
|
|
182
|
+
? "text-blue-600 dark:text-blue-400"
|
|
183
|
+
: "text-zinc-400"
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{item.icon}
|
|
187
|
+
</span>
|
|
188
|
+
{!collapsed && <span className="truncate">{item.label}</span>}
|
|
189
|
+
</span>
|
|
190
|
+
|
|
191
|
+
{!collapsed && (
|
|
192
|
+
<span className="flex items-center gap-1.5">
|
|
193
|
+
{item.badge && (
|
|
194
|
+
<span className="rounded bg-zinc-200/80 px-1.5 py-0.5 text-[10px] text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400">
|
|
195
|
+
{item.badge}
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
{hasChildren && (
|
|
199
|
+
<ChevronDown
|
|
200
|
+
className={cn(
|
|
201
|
+
"h-3.5 w-3.5 text-zinc-400 transition-transform",
|
|
202
|
+
isOpen ? "rotate-0" : "-rotate-90"
|
|
203
|
+
)}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
208
|
+
</button>
|
|
209
|
+
|
|
210
|
+
{!collapsed && hasChildren && isOpen && (
|
|
211
|
+
<div className="ml-4 border-l border-zinc-200 pl-2.5 space-y-0.5 dark:border-zinc-800">
|
|
212
|
+
{item.children!.map((sub) => {
|
|
213
|
+
const subActive = currentId === sub.id
|
|
214
|
+
return (
|
|
215
|
+
<button
|
|
216
|
+
key={sub.id}
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={() => handleSelect(sub.id)}
|
|
219
|
+
className={cn(
|
|
220
|
+
"flex w-full items-center rounded-md px-2.5 py-1.5 text-xs transition-colors cursor-pointer",
|
|
221
|
+
subActive
|
|
222
|
+
? "text-blue-600 font-medium dark:text-blue-400"
|
|
223
|
+
: "text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
226
|
+
{sub.label}
|
|
227
|
+
</button>
|
|
228
|
+
)
|
|
229
|
+
})}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Footer User Pod */}
|
|
238
|
+
<div className="border-t border-zinc-200 p-3 dark:border-zinc-800">
|
|
239
|
+
<div className="flex items-center justify-between gap-2.5">
|
|
240
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
241
|
+
<Avatar fallback="SR" tint="blue" size="sm" status="online" />
|
|
242
|
+
{!collapsed && (
|
|
243
|
+
<div className="min-w-0">
|
|
244
|
+
<p className="truncate text-xs font-medium text-zinc-900 dark:text-zinc-100">
|
|
245
|
+
Saeful Rohman
|
|
246
|
+
</p>
|
|
247
|
+
<p className="truncate text-[11px] text-zinc-500">
|
|
248
|
+
saeful@acme.com
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</aside>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface HeaderNavbarProps {
|
|
260
|
+
breadcrumbs?: string[]
|
|
261
|
+
onOpenCommandPalette?: () => void
|
|
262
|
+
className?: string
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const HeaderNavbar: React.FC<HeaderNavbarProps> = ({
|
|
266
|
+
breadcrumbs = ["Workspace", "Analytics", "Overview"],
|
|
267
|
+
onOpenCommandPalette,
|
|
268
|
+
className,
|
|
269
|
+
}) => {
|
|
270
|
+
const [internalCmdOpen, setInternalCmdOpen] = React.useState(false)
|
|
271
|
+
|
|
272
|
+
const handleOpenSearch = () => {
|
|
273
|
+
if (onOpenCommandPalette) {
|
|
274
|
+
onOpenCommandPalette()
|
|
275
|
+
} else {
|
|
276
|
+
setInternalCmdOpen(true)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<>
|
|
282
|
+
<header
|
|
283
|
+
className={cn(
|
|
284
|
+
"flex h-14 w-full items-center justify-between border-b border-zinc-200 bg-white px-5 dark:border-zinc-800 dark:bg-zinc-900",
|
|
285
|
+
className
|
|
286
|
+
)}
|
|
287
|
+
>
|
|
288
|
+
{/* Breadcrumb Trail */}
|
|
289
|
+
<nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-xs">
|
|
290
|
+
{breadcrumbs.map((crumb, idx) => {
|
|
291
|
+
const isLast = idx === breadcrumbs.length - 1
|
|
292
|
+
return (
|
|
293
|
+
<React.Fragment key={crumb}>
|
|
294
|
+
{idx > 0 && (
|
|
295
|
+
<ChevronRight className="h-3.5 w-3.5 text-zinc-400 shrink-0" />
|
|
296
|
+
)}
|
|
297
|
+
<span
|
|
298
|
+
className={cn(
|
|
299
|
+
"truncate",
|
|
300
|
+
isLast
|
|
301
|
+
? "font-medium text-zinc-900 dark:text-zinc-100"
|
|
302
|
+
: "text-zinc-500 dark:text-zinc-400"
|
|
303
|
+
)}
|
|
304
|
+
>
|
|
305
|
+
{crumb}
|
|
306
|
+
</span>
|
|
307
|
+
</React.Fragment>
|
|
308
|
+
)
|
|
309
|
+
})}
|
|
310
|
+
</nav>
|
|
311
|
+
|
|
312
|
+
{/* Right Controls */}
|
|
313
|
+
<div className="flex items-center gap-2.5">
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
onClick={handleOpenSearch}
|
|
317
|
+
className="hidden sm:flex h-8 w-52 items-center justify-between rounded-lg border border-zinc-200 bg-zinc-50 px-2.5 text-xs text-zinc-500 transition-colors hover:border-zinc-300 hover:text-zinc-800 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:text-zinc-100 cursor-pointer"
|
|
318
|
+
>
|
|
319
|
+
<span className="flex items-center gap-2">
|
|
320
|
+
<Search className="h-3.5 w-3.5 text-zinc-400 dark:text-zinc-400" />
|
|
321
|
+
<span>Quick search...</span>
|
|
322
|
+
</span>
|
|
323
|
+
<kbd className="rounded border border-zinc-200 bg-white px-1.5 py-0.5 font-mono text-[10px] text-zinc-500 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300">
|
|
324
|
+
⌘K
|
|
325
|
+
</kbd>
|
|
326
|
+
</button>
|
|
327
|
+
|
|
328
|
+
<button
|
|
329
|
+
type="button"
|
|
330
|
+
aria-label="Notifications"
|
|
331
|
+
className="relative flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-200 bg-white text-zinc-500 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700 cursor-pointer"
|
|
332
|
+
>
|
|
333
|
+
<Bell className="h-3.5 w-3.5" />
|
|
334
|
+
<span className="absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full bg-blue-600" />
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
</header>
|
|
338
|
+
|
|
339
|
+
{!onOpenCommandPalette && (
|
|
340
|
+
<CommandPalette
|
|
341
|
+
open={internalCmdOpen}
|
|
342
|
+
onOpenChange={setInternalCmdOpen}
|
|
343
|
+
enableGlobalShortcut={false}
|
|
344
|
+
/>
|
|
345
|
+
)}
|
|
346
|
+
</>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface PageHeaderBarProps {
|
|
351
|
+
title: string
|
|
352
|
+
subtitle?: string
|
|
353
|
+
badge?: string
|
|
354
|
+
actions?: React.ReactNode
|
|
355
|
+
className?: string
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export const PageHeaderBar: React.FC<PageHeaderBarProps> = ({
|
|
359
|
+
title,
|
|
360
|
+
subtitle,
|
|
361
|
+
badge,
|
|
362
|
+
actions,
|
|
363
|
+
className,
|
|
364
|
+
}) => {
|
|
365
|
+
return (
|
|
366
|
+
<div
|
|
367
|
+
className={cn(
|
|
368
|
+
"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between border-b border-zinc-200 pb-5 dark:border-zinc-800",
|
|
369
|
+
className
|
|
370
|
+
)}
|
|
371
|
+
>
|
|
372
|
+
<div className="space-y-1">
|
|
373
|
+
<div className="flex items-center gap-2.5">
|
|
374
|
+
<h2 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
|
|
375
|
+
{title}
|
|
376
|
+
</h2>
|
|
377
|
+
{badge && (
|
|
378
|
+
<Badge variant="blue" dot>
|
|
379
|
+
{badge}
|
|
380
|
+
</Badge>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
{subtitle && (
|
|
384
|
+
<p className="text-xs text-zinc-500 dark:text-zinc-400">{subtitle}</p>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
<div className="flex items-center gap-2">
|
|
388
|
+
{actions || (
|
|
389
|
+
<>
|
|
390
|
+
<Button variant="outline" size="sm">
|
|
391
|
+
Export report
|
|
392
|
+
</Button>
|
|
393
|
+
<Button variant="primary" size="sm">
|
|
394
|
+
New report
|
|
395
|
+
</Button>
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export interface AppShellProps {
|
|
404
|
+
workspaceName?: string
|
|
405
|
+
planBadge?: string
|
|
406
|
+
breadcrumbs?: string[]
|
|
407
|
+
user?: {
|
|
408
|
+
name: string
|
|
409
|
+
email: string
|
|
410
|
+
initials: string
|
|
411
|
+
avatarSrc?: string
|
|
412
|
+
}
|
|
413
|
+
children?: React.ReactNode
|
|
414
|
+
className?: string
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export const AppShell: React.FC<AppShellProps> = ({
|
|
418
|
+
workspaceName = "Acme Corp",
|
|
419
|
+
planBadge = "Pro",
|
|
420
|
+
breadcrumbs = ["Workspace", "Overview"],
|
|
421
|
+
children,
|
|
422
|
+
className,
|
|
423
|
+
}) => {
|
|
424
|
+
return (
|
|
425
|
+
<div
|
|
426
|
+
className={cn(
|
|
427
|
+
"flex min-h-[600px] w-full overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-950",
|
|
428
|
+
className
|
|
429
|
+
)}
|
|
430
|
+
>
|
|
431
|
+
<SidebarShell
|
|
432
|
+
workspaceName={workspaceName}
|
|
433
|
+
workspaceTier={planBadge}
|
|
434
|
+
/>
|
|
435
|
+
<div className="flex flex-1 flex-col min-w-0">
|
|
436
|
+
<HeaderNavbar breadcrumbs={breadcrumbs} />
|
|
437
|
+
<main className="flex-1 p-6 space-y-6 overflow-y-auto">{children}</main>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
440
|
+
)
|
|
441
|
+
}
|