@olwiba/ui 0.0.28
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 +98 -0
- package/dist/index.d.ts +762 -0
- package/dist/index.js +2102 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
- package/src/app/AppShell.tsx +365 -0
- package/src/app/AuthSection.tsx +141 -0
- package/src/app/EmptyState.tsx +34 -0
- package/src/app/ErrorPage.tsx +78 -0
- package/src/app/UpgradePrompt.tsx +203 -0
- package/src/blog/PostCard.tsx +66 -0
- package/src/blog/PostList.tsx +27 -0
- package/src/components/ConfirmDialog.tsx +43 -0
- package/src/components/ContextMenu.tsx +86 -0
- package/src/components/DevBanner.tsx +22 -0
- package/src/components/Dock.tsx +94 -0
- package/src/components/FeatureCard.tsx +45 -0
- package/src/components/GlassCard.tsx +30 -0
- package/src/components/ImageCard.tsx +60 -0
- package/src/components/PageHeader.tsx +101 -0
- package/src/components/PricingCard.tsx +90 -0
- package/src/components/RegisterHotkeys.tsx +43 -0
- package/src/components/RootErrorFallback.tsx +29 -0
- package/src/components/Spinner.tsx +14 -0
- package/src/components/Spotlight.tsx +104 -0
- package/src/components/StatCard.tsx +44 -0
- package/src/components/Suspensed.tsx +17 -0
- package/src/components/TestimonialCard.tsx +64 -0
- package/src/components/ThemeColorUpdater.tsx +23 -0
- package/src/components/ThemeSwitchMinimal.tsx +28 -0
- package/src/components/VersionBanner.tsx +38 -0
- package/src/context/OlwibaUIContext.tsx +58 -0
- package/src/hooks/use-confirm.ts +64 -0
- package/src/hooks/use-controlled-open.ts +33 -0
- package/src/hooks/use-copy-to-clipboard.ts +20 -0
- package/src/hooks/use-debounce.ts +14 -0
- package/src/hooks/use-intersection-observer.ts +25 -0
- package/src/hooks/use-local-storage.ts +31 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-mounted.ts +11 -0
- package/src/hooks/use-pagination.ts +31 -0
- package/src/hooks/use-scrolled-past.ts +27 -0
- package/src/index.ts +113 -0
- package/src/lib/utils.ts +6 -0
- package/src/marketing/ContactSection.tsx +110 -0
- package/src/marketing/CtaSection.tsx +74 -0
- package/src/marketing/FaqSection.tsx +44 -0
- package/src/marketing/FeaturesSection.tsx +42 -0
- package/src/marketing/Footer.tsx +87 -0
- package/src/marketing/HeroSection.tsx +112 -0
- package/src/marketing/LogoStrip.tsx +94 -0
- package/src/marketing/Navbar.tsx +142 -0
- package/src/marketing/NewsletterSection.tsx +64 -0
- package/src/marketing/PricingSection.tsx +120 -0
- package/src/marketing/SectionTitle.tsx +30 -0
- package/src/marketing/StatsSection.tsx +63 -0
- package/src/marketing/TeamSection.tsx +110 -0
- package/src/marketing/TestimonialsSection.tsx +56 -0
- package/src/motion/CountUp.tsx +64 -0
- package/src/motion/FadeIn.tsx +69 -0
- package/src/motion/PageTransition.tsx +49 -0
- package/src/motion/StaggerChildren.tsx +74 -0
- package/src/overlays/Overlay.tsx +88 -0
- package/src/overlays/Underlay.tsx +114 -0
- package/src/primitives/Badge.tsx +11 -0
- package/src/primitives/Button.tsx +16 -0
- package/src/primitives/Card.tsx +22 -0
- package/src/primitives/Checkbox.tsx +17 -0
- package/src/primitives/Input.tsx +16 -0
- package/src/primitives/Switch.tsx +16 -0
- package/src/primitives/Textarea.tsx +16 -0
- package/src/primitives/index.ts +7 -0
- package/src/types/external-packages.d.ts +79 -0
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@olwiba/ui",
|
|
3
|
+
"version": "0.0.28",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"description": "App shells, marketing sections, interactive components, and hooks built on @olwiba/cn",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"dev:link:ecosystem": "pwsh -NoProfile -Command '& { $ErrorActionPreference = \"Stop\"; function Invoke-Link([string]$RepoPath,[string[]]$CommandArgs){ Write-Host (\"[link-ecosystem] bun \" + ($CommandArgs -join \" \") + \" (\" + $RepoPath + \")\"); Push-Location $RepoPath; try { & bun @CommandArgs } finally { Pop-Location } }; Invoke-Link \"..\\\\olwibaCN\" @(\"link\"); Invoke-Link \"..\\\\olwibaDOCS\" @(\"link\"); Invoke-Link \".\" @(\"link\"); Invoke-Link \"..\\\\genesis-render\" @(\"link\"); Invoke-Link \"..\\\\olwibaDOCS\" @(\"link\",\"@olwiba/cn\"); Invoke-Link \".\" @(\"link\",\"@olwiba/cn\"); Invoke-Link \".\" @(\"link\",\"@olwiba/docs\"); Invoke-Link \"..\\\\genesis-render\" @(\"link\",\"@olwiba/cn\"); Invoke-Link \"..\\\\genesis-render\" @(\"link\",\"@olwiba/docs\"); Invoke-Link \"..\\\\genesis-render\" @(\"link\",\"@olwiba/ui\"); Write-Host \"[link-ecosystem] Local Bun links are in place. CI and build servers will still use published package versions.\" }'",
|
|
23
|
+
"dev:link:ecosystem:check": "pwsh -NoProfile -Command '& { $ErrorActionPreference = \"Stop\"; function Show-Link([string]$RepoPath,[string[]]$CommandArgs){ Write-Host (\"[link-ecosystem] bun \" + ($CommandArgs -join \" \") + \" (\" + $RepoPath + \")\") }; Show-Link \"..\\\\olwibaCN\" @(\"link\"); Show-Link \"..\\\\olwibaDOCS\" @(\"link\"); Show-Link \".\" @(\"link\"); Show-Link \"..\\\\genesis-render\" @(\"link\"); Show-Link \"..\\\\olwibaDOCS\" @(\"link\",\"@olwiba/cn\"); Show-Link \".\" @(\"link\",\"@olwiba/cn\"); Show-Link \".\" @(\"link\",\"@olwiba/docs\"); Show-Link \"..\\\\genesis-render\" @(\"link\",\"@olwiba/cn\"); Show-Link \"..\\\\genesis-render\" @(\"link\",\"@olwiba/docs\"); Show-Link \"..\\\\genesis-render\" @(\"link\",\"@olwiba/ui\"); Write-Host \"[link-ecosystem] Skipped directories without package.json: genesis, genesis-start, genesis-sync\"; Write-Host \"[link-ecosystem] Dry run complete. No links were changed.\" }'",
|
|
24
|
+
"web:build": "fumadocs-mdx && vite build",
|
|
25
|
+
"web:dev": "fumadocs-mdx && vite dev",
|
|
26
|
+
"start": "bun run server.ts",
|
|
27
|
+
"types:check": "fumadocs-mdx && tsc --noEmit",
|
|
28
|
+
"iso:generate": "bun scripts/generate-iso-previews.ts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@dnd-kit/core": "^6.3.1",
|
|
32
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
33
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
34
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
35
|
+
"@olwiba/docs": "^0.1.25",
|
|
36
|
+
"@olwiba/dx": "^0.0.10",
|
|
37
|
+
"@tanstack/react-table": "^8.21.3",
|
|
38
|
+
"clsx": "^2.1.1",
|
|
39
|
+
"lucide-react": "^0.562.0",
|
|
40
|
+
"recharts": "^3.8.0",
|
|
41
|
+
"sonner": "^2.0.7",
|
|
42
|
+
"tailwind-merge": "^3.4.0",
|
|
43
|
+
"zod": "^4.3.6"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@olwiba/cn": ">=0.1.2",
|
|
47
|
+
"react": ">=18.0.0",
|
|
48
|
+
"react-dom": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@olwiba/cn": "^0.1.15",
|
|
52
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
53
|
+
"@tanstack/react-router": "1.154.8",
|
|
54
|
+
"@tanstack/react-router-devtools": "1.154.8",
|
|
55
|
+
"@tanstack/react-start": "1.154.8",
|
|
56
|
+
"@types/mdx": "^2.0.13",
|
|
57
|
+
"@types/node": "^22.5.4",
|
|
58
|
+
"@types/react": "^19.0.8",
|
|
59
|
+
"@types/react-dom": "^19.0.3",
|
|
60
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
61
|
+
"fumadocs-core": "^16.4.7",
|
|
62
|
+
"fumadocs-mdx": "^14.2.6",
|
|
63
|
+
"fumadocs-ui": "^16.4.7",
|
|
64
|
+
"next-themes": "^0.4.6",
|
|
65
|
+
"puppeteer-core": "^24.0.0",
|
|
66
|
+
"react": "^19.0.0",
|
|
67
|
+
"react-dom": "^19.0.0",
|
|
68
|
+
"react-resizable-panels": "^4.4.1",
|
|
69
|
+
"rehype-pretty-code": "^0.14.0",
|
|
70
|
+
"shiki": "^3.0.0",
|
|
71
|
+
"tailwindcss": "^4.1.18",
|
|
72
|
+
"tsup": "^8.4.0",
|
|
73
|
+
"tw-animate-css": "^1.4.0",
|
|
74
|
+
"typescript": "^5.7.2",
|
|
75
|
+
"vite": "^7.3.1",
|
|
76
|
+
"vite-tsconfig-paths": "^5.1.4"
|
|
77
|
+
},
|
|
78
|
+
"repository": {
|
|
79
|
+
"type": "git",
|
|
80
|
+
"url": "https://github.com/Olwiba/olwibaUI"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"registry": "https://registry.npmjs.org/",
|
|
84
|
+
"access": "public"
|
|
85
|
+
},
|
|
86
|
+
"author": "Olwiba",
|
|
87
|
+
"license": "MIT"
|
|
88
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { LucideIcon } from 'lucide-react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
BellIcon,
|
|
7
|
+
CreditCardIcon,
|
|
8
|
+
LogOutIcon,
|
|
9
|
+
MoreVerticalIcon,
|
|
10
|
+
PlusCircleIcon,
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
import {
|
|
13
|
+
Avatar,
|
|
14
|
+
AvatarFallback,
|
|
15
|
+
AvatarImage,
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuGroup,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuLabel,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
Separator,
|
|
24
|
+
Sidebar,
|
|
25
|
+
SidebarContent,
|
|
26
|
+
SidebarFooter,
|
|
27
|
+
SidebarGroup,
|
|
28
|
+
SidebarGroupContent,
|
|
29
|
+
SidebarHeader,
|
|
30
|
+
SidebarInset,
|
|
31
|
+
SidebarMenu,
|
|
32
|
+
SidebarMenuButton,
|
|
33
|
+
SidebarMenuItem,
|
|
34
|
+
SidebarProvider,
|
|
35
|
+
SidebarTrigger,
|
|
36
|
+
useSidebar,
|
|
37
|
+
} from '@olwiba/cn';
|
|
38
|
+
import { useUIMode } from '../context/OlwibaUIContext';
|
|
39
|
+
|
|
40
|
+
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export interface AppNavItem {
|
|
43
|
+
icon: LucideIcon;
|
|
44
|
+
label: string;
|
|
45
|
+
href: string;
|
|
46
|
+
isActive?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AppShellAction {
|
|
50
|
+
icon?: LucideIcon;
|
|
51
|
+
label: string;
|
|
52
|
+
href?: string;
|
|
53
|
+
onClick?: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AppShellUser {
|
|
57
|
+
email: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
avatar?: string;
|
|
60
|
+
/** Displayed as a sub-label (e.g. plan tier). */
|
|
61
|
+
plan?: string;
|
|
62
|
+
onSignOut?: () => void;
|
|
63
|
+
onBilling?: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AppShellBrand {
|
|
67
|
+
name: ReactNode;
|
|
68
|
+
logo?: ReactNode;
|
|
69
|
+
href?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Render a navigation link. Override to integrate your router's Link component.
|
|
74
|
+
* @example renderLink={({ href, children }) => <Link to={href}>{children}</Link>}
|
|
75
|
+
*/
|
|
76
|
+
export type AppShellRenderLink = (props: {
|
|
77
|
+
href: string;
|
|
78
|
+
children: ReactNode;
|
|
79
|
+
className?: string;
|
|
80
|
+
}) => ReactNode;
|
|
81
|
+
|
|
82
|
+
export interface AppShellProps {
|
|
83
|
+
brand?: AppShellBrand;
|
|
84
|
+
navItems?: AppNavItem[];
|
|
85
|
+
/** Primary CTA rendered above nav (e.g. "New project", "Quick create") */
|
|
86
|
+
action?: AppShellAction;
|
|
87
|
+
user?: AppShellUser;
|
|
88
|
+
/** Text shown in the top header bar */
|
|
89
|
+
pageTitle?: string;
|
|
90
|
+
/** Override link rendering for SPA navigation. Defaults to a native <a>. */
|
|
91
|
+
renderLink?: AppShellRenderLink;
|
|
92
|
+
children?: ReactNode;
|
|
93
|
+
/**
|
|
94
|
+
* Sidebar collapse behaviour.
|
|
95
|
+
* - `"icon"` — collapses to a narrow icon rail (default, recommended for desktop)
|
|
96
|
+
* - `"offcanvas"` — slides off-screen as a drawer overlay
|
|
97
|
+
* - `"none"` — always fully visible
|
|
98
|
+
*/
|
|
99
|
+
collapsible?: 'icon' | 'offcanvas' | 'none';
|
|
100
|
+
/**
|
|
101
|
+
* Layout scope.
|
|
102
|
+
* - `"viewport"` (default) — fills the full browser viewport; use for top-level app shells.
|
|
103
|
+
* - `"contained"` — fills its parent container; use in docs sandboxes, modals, or embedded previews.
|
|
104
|
+
*/
|
|
105
|
+
sidebarPosition?: 'viewport' | 'contained';
|
|
106
|
+
/** Which side the sidebar sits on. @default "left" */
|
|
107
|
+
side?: 'left' | 'right';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Internal sub-components ──────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function NavUser({ user }: { user: AppShellUser }) {
|
|
113
|
+
const { isMobile } = useSidebar();
|
|
114
|
+
const uiMode = useUIMode();
|
|
115
|
+
const avatarMode = uiMode !== 'default' ? (uiMode as 'playful' | 'smooth') : undefined;
|
|
116
|
+
const initials = (user.name ?? user.email).slice(0, 2).toUpperCase();
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<SidebarMenu>
|
|
120
|
+
<SidebarMenuItem>
|
|
121
|
+
<DropdownMenu>
|
|
122
|
+
<DropdownMenuTrigger asChild>
|
|
123
|
+
<SidebarMenuButton
|
|
124
|
+
size="lg"
|
|
125
|
+
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
126
|
+
>
|
|
127
|
+
<Avatar mode={avatarMode} size="sm" className="grayscale">
|
|
128
|
+
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
129
|
+
<AvatarFallback>{initials}</AvatarFallback>
|
|
130
|
+
</Avatar>
|
|
131
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
132
|
+
<span className="truncate font-medium">{user.name ?? user.email}</span>
|
|
133
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
134
|
+
{user.plan ? `${user.plan} plan` : user.email}
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
<MoreVerticalIcon className="ml-auto size-4" />
|
|
138
|
+
</SidebarMenuButton>
|
|
139
|
+
</DropdownMenuTrigger>
|
|
140
|
+
<DropdownMenuContent
|
|
141
|
+
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
|
142
|
+
side={isMobile ? 'bottom' : 'right'}
|
|
143
|
+
align="end"
|
|
144
|
+
sideOffset={4}
|
|
145
|
+
>
|
|
146
|
+
<DropdownMenuLabel className="p-0 font-normal">
|
|
147
|
+
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
148
|
+
<Avatar mode={avatarMode} size="sm">
|
|
149
|
+
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
150
|
+
<AvatarFallback>{initials}</AvatarFallback>
|
|
151
|
+
</Avatar>
|
|
152
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
153
|
+
<span className="truncate font-medium">{user.name ?? user.email}</span>
|
|
154
|
+
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</DropdownMenuLabel>
|
|
158
|
+
<DropdownMenuSeparator />
|
|
159
|
+
<DropdownMenuGroup>
|
|
160
|
+
{user.onBilling && (
|
|
161
|
+
<DropdownMenuItem onClick={user.onBilling}>
|
|
162
|
+
<CreditCardIcon />
|
|
163
|
+
Billing
|
|
164
|
+
</DropdownMenuItem>
|
|
165
|
+
)}
|
|
166
|
+
<DropdownMenuItem>
|
|
167
|
+
<BellIcon />
|
|
168
|
+
Notifications
|
|
169
|
+
</DropdownMenuItem>
|
|
170
|
+
</DropdownMenuGroup>
|
|
171
|
+
{user.onSignOut && (
|
|
172
|
+
<>
|
|
173
|
+
<DropdownMenuSeparator />
|
|
174
|
+
<DropdownMenuItem onClick={user.onSignOut}>
|
|
175
|
+
<LogOutIcon />
|
|
176
|
+
Sign out
|
|
177
|
+
</DropdownMenuItem>
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
180
|
+
</DropdownMenuContent>
|
|
181
|
+
</DropdownMenu>
|
|
182
|
+
</SidebarMenuItem>
|
|
183
|
+
</SidebarMenu>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ShellSidebar({
|
|
188
|
+
brand,
|
|
189
|
+
navItems,
|
|
190
|
+
action,
|
|
191
|
+
user,
|
|
192
|
+
renderLink,
|
|
193
|
+
collapsible,
|
|
194
|
+
side,
|
|
195
|
+
sidebarPosition,
|
|
196
|
+
}: {
|
|
197
|
+
brand: AppShellBrand;
|
|
198
|
+
navItems: AppNavItem[];
|
|
199
|
+
action?: AppShellAction;
|
|
200
|
+
user: AppShellUser;
|
|
201
|
+
renderLink: AppShellRenderLink;
|
|
202
|
+
collapsible: 'icon' | 'offcanvas' | 'none';
|
|
203
|
+
side: 'left' | 'right';
|
|
204
|
+
sidebarPosition: 'viewport' | 'contained';
|
|
205
|
+
}) {
|
|
206
|
+
return (
|
|
207
|
+
<Sidebar side={side} collapsible={collapsible} sidebarPosition={sidebarPosition}>
|
|
208
|
+
<SidebarHeader>
|
|
209
|
+
<SidebarMenu>
|
|
210
|
+
<SidebarMenuItem>
|
|
211
|
+
<SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:!p-1.5">
|
|
212
|
+
{renderLink({
|
|
213
|
+
href: brand.href ?? '#',
|
|
214
|
+
children: (
|
|
215
|
+
<>
|
|
216
|
+
{brand.logo}
|
|
217
|
+
<span className="text-base font-semibold">{brand.name}</span>
|
|
218
|
+
</>
|
|
219
|
+
),
|
|
220
|
+
})}
|
|
221
|
+
</SidebarMenuButton>
|
|
222
|
+
</SidebarMenuItem>
|
|
223
|
+
</SidebarMenu>
|
|
224
|
+
</SidebarHeader>
|
|
225
|
+
|
|
226
|
+
<SidebarContent>
|
|
227
|
+
<SidebarGroup>
|
|
228
|
+
<SidebarGroupContent className="flex flex-col gap-2">
|
|
229
|
+
{action && (
|
|
230
|
+
<SidebarMenu>
|
|
231
|
+
<SidebarMenuItem>
|
|
232
|
+
{action.href ? (
|
|
233
|
+
<SidebarMenuButton
|
|
234
|
+
asChild
|
|
235
|
+
tooltip={action.label}
|
|
236
|
+
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
|
237
|
+
>
|
|
238
|
+
{renderLink({
|
|
239
|
+
href: action.href,
|
|
240
|
+
children: (
|
|
241
|
+
<>
|
|
242
|
+
{action.icon && <action.icon />}
|
|
243
|
+
<span>{action.label}</span>
|
|
244
|
+
</>
|
|
245
|
+
),
|
|
246
|
+
})}
|
|
247
|
+
</SidebarMenuButton>
|
|
248
|
+
) : (
|
|
249
|
+
<SidebarMenuButton
|
|
250
|
+
tooltip={action.label}
|
|
251
|
+
onClick={action.onClick}
|
|
252
|
+
className="min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground"
|
|
253
|
+
>
|
|
254
|
+
{action.icon && <action.icon />}
|
|
255
|
+
<span>{action.label}</span>
|
|
256
|
+
</SidebarMenuButton>
|
|
257
|
+
)}
|
|
258
|
+
</SidebarMenuItem>
|
|
259
|
+
</SidebarMenu>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
<SidebarMenu>
|
|
263
|
+
{navItems.map((item) => (
|
|
264
|
+
<SidebarMenuItem key={item.label}>
|
|
265
|
+
<SidebarMenuButton asChild tooltip={item.label} isActive={item.isActive}>
|
|
266
|
+
{renderLink({
|
|
267
|
+
href: item.href,
|
|
268
|
+
children: (
|
|
269
|
+
<>
|
|
270
|
+
<item.icon />
|
|
271
|
+
<span>{item.label}</span>
|
|
272
|
+
</>
|
|
273
|
+
),
|
|
274
|
+
})}
|
|
275
|
+
</SidebarMenuButton>
|
|
276
|
+
</SidebarMenuItem>
|
|
277
|
+
))}
|
|
278
|
+
</SidebarMenu>
|
|
279
|
+
</SidebarGroupContent>
|
|
280
|
+
</SidebarGroup>
|
|
281
|
+
</SidebarContent>
|
|
282
|
+
|
|
283
|
+
<SidebarFooter>
|
|
284
|
+
<NavUser user={user} />
|
|
285
|
+
</SidebarFooter>
|
|
286
|
+
</Sidebar>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function ShellHeader({ pageTitle }: { pageTitle: string }) {
|
|
291
|
+
return (
|
|
292
|
+
<header className="flex h-12 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
|
|
293
|
+
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
294
|
+
<SidebarTrigger className="-ml-1" />
|
|
295
|
+
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
|
296
|
+
<h1 className="text-base font-medium">{pageTitle}</h1>
|
|
297
|
+
</div>
|
|
298
|
+
</header>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Default placeholder values (keeps the shell renderable without required props) ─
|
|
303
|
+
|
|
304
|
+
const defaultBrand: AppShellBrand = {
|
|
305
|
+
name: 'App',
|
|
306
|
+
href: '#',
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const defaultUser: AppShellUser = {
|
|
310
|
+
name: 'User',
|
|
311
|
+
email: 'user@example.com',
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
|
|
315
|
+
<a href={href} className={className}>{children}</a>
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
export function AppShell({
|
|
321
|
+
brand = defaultBrand,
|
|
322
|
+
navItems = [],
|
|
323
|
+
action,
|
|
324
|
+
user = defaultUser,
|
|
325
|
+
pageTitle = 'Dashboard',
|
|
326
|
+
renderLink = defaultRenderLink,
|
|
327
|
+
collapsible = 'icon',
|
|
328
|
+
sidebarPosition = 'viewport',
|
|
329
|
+
side = 'left',
|
|
330
|
+
children,
|
|
331
|
+
}: AppShellProps = {}) {
|
|
332
|
+
const isContained = sidebarPosition === 'contained';
|
|
333
|
+
|
|
334
|
+
const inner = (
|
|
335
|
+
<SidebarProvider
|
|
336
|
+
layout={isContained ? 'embedded' : 'viewport'}
|
|
337
|
+
className={isContained ? 'overflow-hidden' : 'h-svh overflow-hidden'}
|
|
338
|
+
>
|
|
339
|
+
<ShellSidebar
|
|
340
|
+
brand={brand}
|
|
341
|
+
navItems={navItems}
|
|
342
|
+
action={action}
|
|
343
|
+
user={user}
|
|
344
|
+
renderLink={renderLink}
|
|
345
|
+
collapsible={collapsible}
|
|
346
|
+
side={side}
|
|
347
|
+
sidebarPosition={sidebarPosition}
|
|
348
|
+
/>
|
|
349
|
+
<SidebarInset className={isContained ? undefined : 'overflow-y-auto'}>
|
|
350
|
+
<ShellHeader pageTitle={pageTitle} />
|
|
351
|
+
{children}
|
|
352
|
+
</SidebarInset>
|
|
353
|
+
</SidebarProvider>
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (isContained) {
|
|
357
|
+
return (
|
|
358
|
+
<div className="relative flex h-full min-h-0 w-full max-w-full flex-col overflow-hidden">
|
|
359
|
+
{inner}
|
|
360
|
+
</div>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return inner;
|
|
365
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { GalleryVerticalEnd, Building2, ShieldCheck, Sparkles } from 'lucide-react';
|
|
5
|
+
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, cn } from '@olwiba/cn';
|
|
6
|
+
|
|
7
|
+
// ─── Centered layout ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function CenteredAuth({ children }: { children: React.ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<section className="flex h-full min-h-[720px] w-full flex-col items-center justify-center gap-6 rounded-2xl bg-muted p-6 md:p-10">
|
|
12
|
+
<div className="flex w-full max-w-sm flex-col gap-6">
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
</section>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Split layout ─────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function SplitAuth({ children, panel }: { children: React.ReactNode; panel?: React.ReactNode }) {
|
|
22
|
+
const defaultPanel = (
|
|
23
|
+
<div className="flex flex-col gap-6">
|
|
24
|
+
<Badge variant="secondary" className="w-fit">Welcome</Badge>
|
|
25
|
+
<div className="space-y-3">
|
|
26
|
+
<h2 className="text-3xl font-semibold tracking-tight">Build better, ship faster</h2>
|
|
27
|
+
<p className="max-w-md text-muted-foreground">
|
|
28
|
+
Sign in to continue building with ready-made components and layouts.
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="space-y-3 text-sm text-muted-foreground">
|
|
32
|
+
<div className="flex items-center gap-2"><ShieldCheck className="size-4" /> Type-safe components</div>
|
|
33
|
+
<div className="flex items-center gap-2"><Sparkles className="size-4" /> Polished defaults</div>
|
|
34
|
+
<div className="flex items-center gap-2"><Building2 className="size-4" /> Plug-and-play shells</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<section className="grid h-full min-h-[560px] overflow-hidden rounded-2xl border bg-card lg:grid-cols-2">
|
|
41
|
+
<div className="relative hidden p-8 pb-16 lg:flex lg:flex-col lg:justify-center">
|
|
42
|
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-muted" />
|
|
43
|
+
<div className="relative z-10">{panel ?? defaultPanel}</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="flex items-center justify-center p-6 lg:p-10">
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Shared form slot ─────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export interface AuthFormProps {
|
|
55
|
+
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
|
|
56
|
+
onSso?: () => void;
|
|
57
|
+
signUpHref?: string;
|
|
58
|
+
forgotPasswordHref?: string;
|
|
59
|
+
brand?: React.ReactNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function DefaultForm({ onSubmit, onSso, signUpHref = '#', forgotPasswordHref = '#', brand }: AuthFormProps) {
|
|
63
|
+
return (
|
|
64
|
+
<Card className="w-full max-w-md">
|
|
65
|
+
<CardHeader>
|
|
66
|
+
{brand && <div className="mb-2">{brand}</div>}
|
|
67
|
+
<CardTitle>Sign in</CardTitle>
|
|
68
|
+
<CardDescription>Enter your email and password to continue.</CardDescription>
|
|
69
|
+
</CardHeader>
|
|
70
|
+
<CardContent className="space-y-4">
|
|
71
|
+
<form onSubmit={onSubmit} className="space-y-4">
|
|
72
|
+
<div className="space-y-2">
|
|
73
|
+
<Label htmlFor="auth-email">Email</Label>
|
|
74
|
+
<Input id="auth-email" type="email" placeholder="name@company.com" />
|
|
75
|
+
</div>
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<div className="flex items-center justify-between">
|
|
78
|
+
<Label htmlFor="auth-password">Password</Label>
|
|
79
|
+
{forgotPasswordHref && (
|
|
80
|
+
<a className="text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground" href={forgotPasswordHref}>
|
|
81
|
+
Forgot password?
|
|
82
|
+
</a>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
<Input id="auth-password" type="password" placeholder="••••••••" />
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex flex-col gap-2">
|
|
88
|
+
<Button type="submit" className="w-full">Continue</Button>
|
|
89
|
+
{onSso && (
|
|
90
|
+
<Button type="button" variant="outline" className="w-full" onClick={onSso}>Use SSO</Button>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</form>
|
|
94
|
+
{signUpHref && (
|
|
95
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
96
|
+
New here?{' '}
|
|
97
|
+
<a className="text-foreground underline underline-offset-4" href={signUpHref}>
|
|
98
|
+
Create an account
|
|
99
|
+
</a>
|
|
100
|
+
</p>
|
|
101
|
+
)}
|
|
102
|
+
</CardContent>
|
|
103
|
+
</Card>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export interface AuthSectionProps extends AuthFormProps {
|
|
110
|
+
/** Visual layout of the auth screen */
|
|
111
|
+
layout?: 'centered' | 'split';
|
|
112
|
+
/** Custom form/card content. Defaults to the built-in sign-in form. */
|
|
113
|
+
children?: React.ReactNode;
|
|
114
|
+
/** (split layout only) Custom content for the left decorative panel */
|
|
115
|
+
panel?: React.ReactNode;
|
|
116
|
+
className?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function AuthSection({
|
|
120
|
+
layout = 'centered',
|
|
121
|
+
children,
|
|
122
|
+
panel,
|
|
123
|
+
className,
|
|
124
|
+
...formProps
|
|
125
|
+
}: AuthSectionProps) {
|
|
126
|
+
const form = children ?? <DefaultForm {...formProps} />;
|
|
127
|
+
|
|
128
|
+
if (layout === 'split') {
|
|
129
|
+
return (
|
|
130
|
+
<div className={cn('h-full', className)}>
|
|
131
|
+
<SplitAuth panel={panel}>{form}</SplitAuth>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className={cn('h-full', className)}>
|
|
138
|
+
<CenteredAuth>{form}</CenteredAuth>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import type { LucideIcon } from 'lucide-react';
|
|
5
|
+
import { cn } from '@olwiba/cn';
|
|
6
|
+
|
|
7
|
+
export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
icon?: LucideIcon;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
action?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function EmptyState({ icon: Icon, title, description, action, className, ...props }: EmptyStateProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={cn('flex flex-col items-center justify-center gap-3 py-12 text-center', className)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{Icon && (
|
|
21
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border bg-muted">
|
|
22
|
+
<Icon className="size-6 text-muted-foreground" />
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
<div className="space-y-1">
|
|
26
|
+
<h3 className="font-semibold">{title}</h3>
|
|
27
|
+
{description && (
|
|
28
|
+
<p className="max-w-xs text-sm text-muted-foreground">{description}</p>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
{action}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|