kofi-stack-template-generator 2.1.16 → 2.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1090,6 +1090,8 @@ import {
1090
1090
  SidebarMenu,
1091
1091
  SidebarMenuButton,
1092
1092
  SidebarMenuItem,
1093
+ SidebarRail,
1094
+ useSidebar,
1093
1095
  } from '@/components/ui/sidebar'
1094
1096
  import {
1095
1097
  DropdownMenu,
@@ -1116,6 +1118,7 @@ const navigation = [
1116
1118
  export function AppSidebar() {
1117
1119
  const router = useRouter()
1118
1120
  const { data: session } = useSession()
1121
+ const { isMobile } = useSidebar()
1119
1122
  const user = session?.user
1120
1123
 
1121
1124
  const handleSignOut = async () => {
@@ -1134,18 +1137,18 @@ export function AppSidebar() {
1134
1137
  }
1135
1138
 
1136
1139
  return (
1137
- <Sidebar>
1140
+ <Sidebar collapsible="icon">
1138
1141
  <SidebarHeader>
1139
1142
  <SidebarMenu>
1140
1143
  <SidebarMenuItem>
1141
- <SidebarMenuButton size="lg" asChild>
1144
+ <SidebarMenuButton size="lg" asChild tooltip="{{projectName}}">
1142
1145
  <a href="/">
1143
- <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
1146
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
1144
1147
  <GalleryVerticalEnd className="size-4" />
1145
1148
  </div>
1146
- <div className="flex flex-col gap-0.5 leading-none">
1147
- <span className="font-semibold">{{projectName}}</span>
1148
- <span className="text-xs text-muted-foreground">Dashboard</span>
1149
+ <div className="grid flex-1 text-left text-sm leading-tight">
1150
+ <span className="truncate font-semibold">{{projectName}}</span>
1151
+ <span className="truncate text-xs">Dashboard</span>
1149
1152
  </div>
1150
1153
  </a>
1151
1154
  </SidebarMenuButton>
@@ -1159,9 +1162,9 @@ export function AppSidebar() {
1159
1162
  <SidebarMenu>
1160
1163
  {navigation.map((item) => (
1161
1164
  <SidebarMenuItem key={item.title}>
1162
- <SidebarMenuButton asChild>
1165
+ <SidebarMenuButton asChild tooltip={item.title}>
1163
1166
  <a href={item.url}>
1164
- <item.icon className="size-4" />
1167
+ <item.icon />
1165
1168
  <span>{item.title}</span>
1166
1169
  </a>
1167
1170
  </SidebarMenuButton>
@@ -1188,7 +1191,7 @@ export function AppSidebar() {
1188
1191
  </Avatar>
1189
1192
  <div className="grid flex-1 text-left text-sm leading-tight">
1190
1193
  <span className="truncate font-semibold">{user?.name ?? 'User'}</span>
1191
- <span className="truncate text-xs text-muted-foreground">
1194
+ <span className="truncate text-xs">
1192
1195
  {user?.email ?? ''}
1193
1196
  </span>
1194
1197
  </div>
@@ -1196,26 +1199,26 @@ export function AppSidebar() {
1196
1199
  </SidebarMenuButton>
1197
1200
  </DropdownMenuTrigger>
1198
1201
  <DropdownMenuContent
1199
- className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
1200
- side="bottom"
1202
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
1203
+ side={isMobile ? 'bottom' : 'right'}
1201
1204
  align="end"
1202
1205
  sideOffset={4}
1203
1206
  >
1204
1207
  <DropdownMenuItem asChild>
1205
1208
  <a href="/settings">
1206
- <User className="mr-2 size-4" />
1209
+ <User />
1207
1210
  Profile
1208
1211
  </a>
1209
1212
  </DropdownMenuItem>
1210
1213
  <DropdownMenuItem asChild>
1211
1214
  <a href="/settings">
1212
- <Settings className="mr-2 size-4" />
1215
+ <Settings />
1213
1216
  Settings
1214
1217
  </a>
1215
1218
  </DropdownMenuItem>
1216
1219
  <DropdownMenuSeparator />
1217
1220
  <DropdownMenuItem onClick={handleSignOut}>
1218
- <LogOut className="mr-2 size-4" />
1221
+ <LogOut />
1219
1222
  Sign out
1220
1223
  </DropdownMenuItem>
1221
1224
  </DropdownMenuContent>
@@ -1223,6 +1226,7 @@ export function AppSidebar() {
1223
1226
  </SidebarMenuItem>
1224
1227
  </SidebarMenu>
1225
1228
  </SidebarFooter>
1229
+ <SidebarRail />
1226
1230
  </Sidebar>
1227
1231
  )
1228
1232
  }
@@ -1249,18 +1253,20 @@ export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayo
1249
1253
  <SidebarProvider>
1250
1254
  <AppSidebar />
1251
1255
  <SidebarInset>
1252
- <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
1253
- <SidebarTrigger className="-ml-1" />
1254
- <Separator orientation="vertical" className="mr-2 h-4" />
1255
- <Breadcrumb>
1256
- <BreadcrumbList>
1257
- <BreadcrumbItem>
1258
- <BreadcrumbPage>{title}</BreadcrumbPage>
1259
- </BreadcrumbItem>
1260
- </BreadcrumbList>
1261
- </Breadcrumb>
1256
+ <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
1257
+ <div className="flex items-center gap-2 px-4">
1258
+ <SidebarTrigger className="-ml-1" />
1259
+ <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
1260
+ <Breadcrumb>
1261
+ <BreadcrumbList>
1262
+ <BreadcrumbItem>
1263
+ <BreadcrumbPage>{title}</BreadcrumbPage>
1264
+ </BreadcrumbItem>
1265
+ </BreadcrumbList>
1266
+ </Breadcrumb>
1267
+ </div>
1262
1268
  </header>
1263
- <main className="flex-1 p-4 md:p-6">{children}</main>
1269
+ <main className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</main>
1264
1270
  </SidebarInset>
1265
1271
  </SidebarProvider>
1266
1272
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kofi-stack-template-generator",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,6 +1,6 @@
1
1
  // Auto-generated file. Do not edit manually.
2
2
  // Run 'pnpm prebuild' to regenerate.
3
- // Generated: 2026-01-15T02:03:58.886Z
3
+ // Generated: 2026-01-15T02:12:38.210Z
4
4
  // Template count: 90
5
5
 
6
6
  export const EMBEDDED_TEMPLATES: Record<string, string> = {
@@ -86,8 +86,8 @@ export const EMBEDDED_TEMPLATES: Record<string, string> = {
86
86
  "web/src/app/page.tsx.hbs": "'use client'\n\nimport { useQuery } from 'convex/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../convex/_generated/api{{/if}}'\nimport { DashboardLayout } from '@/components/dashboard/dashboard-layout'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Skeleton } from '@/components/ui/skeleton'\n\nexport default function HomePage() {\n const user = useQuery(api.users.viewer)\n\n if (user === undefined) {\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <Skeleton className=\"h-8 w-64\" />\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n </DashboardLayout>\n )\n }\n\n return (\n <DashboardLayout title=\"Dashboard\">\n <div className=\"space-y-6\">\n <div>\n <h1 className=\"text-3xl font-bold tracking-tight\">\n Welcome back{user?.name ? `, ${user.name}` : ''}!\n </h1>\n <p className=\"text-muted-foreground\">\n Here's what's happening with your project today.\n </p>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n <Card>\n <CardHeader>\n <CardTitle>Getting Started</CardTitle>\n <CardDescription>Quick start guide for your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Customize your dashboard layout</li>\n <li>Add new pages to the sidebar</li>\n <li>Connect your data sources</li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Documentation</CardTitle>\n <CardDescription>Learn how to build with Kofi Stack</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>\n <a\n href=\"https://docs.convex.dev\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Convex Documentation\n </a>\n </li>\n <li>\n <a\n href=\"https://ui.shadcn.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n shadcn/ui Components\n </a>\n </li>\n <li>\n <a\n href=\"https://nextjs.org/docs\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-primary hover:underline\"\n >\n Next.js Documentation\n </a>\n </li>\n </ul>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Your Stack</CardTitle>\n <CardDescription>Technologies powering your app</CardDescription>\n </CardHeader>\n <CardContent>\n <ul className=\"list-disc list-inside space-y-1 text-sm text-muted-foreground\">\n <li>Next.js 15 with App Router</li>\n <li>Convex for backend & database</li>\n <li>Convex Auth for authentication</li>\n <li>shadcn/ui components</li>\n <li>Tailwind CSS for styling</li>\n </ul>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground\">\n Created with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-primary hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </DashboardLayout>\n )\n}\n",
87
87
  "web/src/components/auth/sign-in-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignInForm() {\n const router = useRouter()\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const result = await signIn.email({\n email,\n password,\n })\n\n if (result.error) {\n setError(result.error.message || 'Invalid email or password')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Invalid email or password')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Welcome back</CardTitle>\n <CardDescription>Sign in to your account to continue</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"Enter your password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Signing in...' : 'Sign In'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>\n <path fill=\"currentColor\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>\n <path fill=\"currentColor\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/>\n <path fill=\"currentColor\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Don&apos;t have an account?{' '}\n <Link href=\"/sign-up\" className=\"text-primary hover:underline font-medium\">\n Sign up\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
88
88
  "web/src/components/auth/sign-up-form.tsx.hbs": "'use client'\n\nimport { useState } from 'react'\nimport { useRouter } from 'next/navigation'\nimport Link from 'next/link'\nimport { signUp, signIn } from '@/lib/auth'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\nimport { Separator } from '@/components/ui/separator'\n\nexport function SignUpForm() {\n const router = useRouter()\n const [name, setName] = useState('')\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n if (password.length < 8) {\n setError('Password must be at least 8 characters')\n setIsLoading(false)\n return\n }\n\n try {\n const result = await signUp.email({\n email,\n password,\n name,\n })\n\n if (result.error) {\n setError(result.error.message || 'Failed to create account')\n } else {\n router.push('/')\n }\n } catch (err) {\n setError('Failed to create account. Email may already be in use.')\n } finally {\n setIsLoading(false)\n }\n }\n\n const handleSocialSignIn = async (provider: 'github' | 'google') => {\n await signIn.social({\n provider,\n callbackURL: '/',\n })\n }\n\n return (\n <Card>\n <CardHeader className=\"text-center\">\n <CardTitle className=\"text-2xl\">Create an account</CardTitle>\n <CardDescription>Enter your details to get started</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"name\">Name</Label>\n <Input\n id=\"name\"\n type=\"text\"\n placeholder=\"Your name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"password\">Password</Label>\n <Input\n id=\"password\"\n type=\"password\"\n placeholder=\"At least 8 characters\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n required\n minLength={8}\n />\n </div>\n\n {error && (\n <p className=\"text-sm text-destructive\">{error}</p>\n )}\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Creating account...' : 'Create Account'}\n </Button>\n </form>\n\n <div className=\"relative my-6\">\n <div className=\"absolute inset-0 flex items-center\">\n <Separator className=\"w-full\" />\n </div>\n <div className=\"relative flex justify-center text-xs uppercase\">\n <span className=\"bg-card px-2 text-muted-foreground\">Or continue with</span>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('github')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\"/>\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" onClick={() => handleSocialSignIn('google')}>\n <svg className=\"mr-2 h-4 w-4\" viewBox=\"0 0 24 24\">\n <path fill=\"currentColor\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z\"/>\n <path fill=\"currentColor\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/>\n <path fill=\"currentColor\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/>\n <path fill=\"currentColor\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/>\n </svg>\n Google\n </Button>\n </div>\n\n <p className=\"mt-6 text-center text-sm text-muted-foreground\">\n Already have an account?{' '}\n <Link href=\"/sign-in\" className=\"text-primary hover:underline font-medium\">\n Sign in\n </Link>\n </p>\n </CardContent>\n </Card>\n )\n}\n",
89
- "web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { signOut, useSession } from '@/lib/auth'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n} from '@/components/ui/sidebar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\n\nconst navigation = [\n {\n title: 'Home',\n url: '/',\n icon: Home,\n },\n {\n title: 'Settings',\n url: '/settings',\n icon: Settings,\n },\n]\n\nexport function AppSidebar() {\n const router = useRouter()\n const { data: session } = useSession()\n const user = session?.user\n\n const handleSignOut = async () => {\n await signOut()\n router.push('/sign-in')\n }\n\n const getInitials = (name?: string | null) => {\n if (!name) return 'U'\n return name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .toUpperCase()\n .slice(0, 2)\n }\n\n return (\n <Sidebar>\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild>\n <a href=\"/\">\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground\">\n <GalleryVerticalEnd className=\"size-4\" />\n </div>\n <div className=\"flex flex-col gap-0.5 leading-none\">\n <span className=\"font-semibold\">{{projectName}}</span>\n <span className=\"text-xs text-muted-foreground\">Dashboard</span>\n </div>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {navigation.map((item) => (\n <SidebarMenuItem key={item.title}>\n <SidebarMenuButton asChild>\n <a href={item.url}>\n <item.icon className=\"size-4\" />\n <span>{item.title}</span>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarFooter>\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n >\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />\n <AvatarFallback className=\"rounded-lg\">\n {getInitials(user?.name)}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{user?.name ?? 'User'}</span>\n <span className=\"truncate text-xs text-muted-foreground\">\n {user?.email ?? ''}\n </span>\n </div>\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg\"\n side=\"bottom\"\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <User className=\"mr-2 size-4\" />\n Profile\n </a>\n </DropdownMenuItem>\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <Settings className=\"mr-2 size-4\" />\n Settings\n </a>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleSignOut}>\n <LogOut className=\"mr-2 size-4\" />\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarFooter>\n </Sidebar>\n )\n}\n",
90
- "web/src/components/dashboard/dashboard-layout.tsx.hbs": "'use client'\n\nimport { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'\nimport { Separator } from '@/components/ui/separator'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbList,\n BreadcrumbPage,\n} from '@/components/ui/breadcrumb'\nimport { AppSidebar } from './app-sidebar'\n\ninterface DashboardLayoutProps {\n children: React.ReactNode\n title?: string\n}\n\nexport function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {\n return (\n <SidebarProvider>\n <AppSidebar />\n <SidebarInset>\n <header className=\"flex h-16 shrink-0 items-center gap-2 border-b px-4\">\n <SidebarTrigger className=\"-ml-1\" />\n <Separator orientation=\"vertical\" className=\"mr-2 h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbPage>{title}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n </header>\n <main className=\"flex-1 p-4 md:p-6\">{children}</main>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n",
89
+ "web/src/components/dashboard/app-sidebar.tsx.hbs": "'use client'\n\nimport {\n GalleryVerticalEnd,\n Home,\n Settings,\n ChevronsUpDown,\n LogOut,\n User,\n} from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { signOut, useSession } from '@/lib/auth'\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarRail,\n useSidebar,\n} from '@/components/ui/sidebar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'\n\nconst navigation = [\n {\n title: 'Home',\n url: '/',\n icon: Home,\n },\n {\n title: 'Settings',\n url: '/settings',\n icon: Settings,\n },\n]\n\nexport function AppSidebar() {\n const router = useRouter()\n const { data: session } = useSession()\n const { isMobile } = useSidebar()\n const user = session?.user\n\n const handleSignOut = async () => {\n await signOut()\n router.push('/sign-in')\n }\n\n const getInitials = (name?: string | null) => {\n if (!name) return 'U'\n return name\n .split(' ')\n .map((n) => n[0])\n .join('')\n .toUpperCase()\n .slice(0, 2)\n }\n\n return (\n <Sidebar collapsible=\"icon\">\n <SidebarHeader>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton size=\"lg\" asChild tooltip=\"{{projectName}}\">\n <a href=\"/\">\n <div className=\"flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground\">\n <GalleryVerticalEnd className=\"size-4\" />\n </div>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{{projectName}}</span>\n <span className=\"truncate text-xs\">Dashboard</span>\n </div>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarHeader>\n <SidebarContent>\n <SidebarGroup>\n <SidebarGroupLabel>Navigation</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {navigation.map((item) => (\n <SidebarMenuItem key={item.title}>\n <SidebarMenuButton asChild tooltip={item.title}>\n <a href={item.url}>\n <item.icon />\n <span>{item.title}</span>\n </a>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroupContent>\n </SidebarGroup>\n </SidebarContent>\n <SidebarFooter>\n <SidebarMenu>\n <SidebarMenuItem>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n >\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user?.image ?? undefined} alt={user?.name ?? 'User'} />\n <AvatarFallback className=\"rounded-lg\">\n {getInitials(user?.name)}\n </AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{user?.name ?? 'User'}</span>\n <span className=\"truncate text-xs\">\n {user?.email ?? ''}\n </span>\n </div>\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg\"\n side={isMobile ? 'bottom' : 'right'}\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <User />\n Profile\n </a>\n </DropdownMenuItem>\n <DropdownMenuItem asChild>\n <a href=\"/settings\">\n <Settings />\n Settings\n </a>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={handleSignOut}>\n <LogOut />\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarFooter>\n <SidebarRail />\n </Sidebar>\n )\n}\n",
90
+ "web/src/components/dashboard/dashboard-layout.tsx.hbs": "'use client'\n\nimport { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'\nimport { Separator } from '@/components/ui/separator'\nimport {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbList,\n BreadcrumbPage,\n} from '@/components/ui/breadcrumb'\nimport { AppSidebar } from './app-sidebar'\n\ninterface DashboardLayoutProps {\n children: React.ReactNode\n title?: string\n}\n\nexport function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayoutProps) {\n return (\n <SidebarProvider>\n <AppSidebar />\n <SidebarInset>\n <header className=\"flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12\">\n <div className=\"flex items-center gap-2 px-4\">\n <SidebarTrigger className=\"-ml-1\" />\n <Separator orientation=\"vertical\" className=\"mr-2 data-[orientation=vertical]:h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbPage>{title}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </Breadcrumb>\n </div>\n </header>\n <main className=\"flex flex-1 flex-col gap-4 p-4 pt-0\">{children}</main>\n </SidebarInset>\n </SidebarProvider>\n )\n}\n",
91
91
  "web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexReactClient } from 'convex/react'\nimport { ConvexBetterAuthProvider } from '@convex-dev/better-auth/react'\nimport { authClient } from '@/lib/auth'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexBetterAuthProvider client={convex} authClient={authClient}>\n {children}\n </ConvexBetterAuthProvider>\n )\n}\n",
92
92
  "web/src/lib/auth-server.ts.hbs": "import { convexBetterAuthNextJs } from '@convex-dev/better-auth/nextjs'\n\nexport const {\n handler,\n preloadAuthQuery,\n isAuthenticated,\n getToken,\n fetchAuthQuery,\n fetchAuthMutation,\n fetchAuthAction,\n} = convexBetterAuthNextJs({\n convexUrl: process.env.NEXT_PUBLIC_CONVEX_URL!,\n convexSiteUrl: process.env.NEXT_PUBLIC_CONVEX_SITE_URL || process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',\n})\n",
93
93
  "web/src/lib/auth.ts.hbs": "'use client'\n\nimport { createAuthClient } from 'better-auth/react'\nimport { convexClient } from '@convex-dev/better-auth/client/plugins'\n\nexport const authClient = createAuthClient({\n plugins: [convexClient()],\n})\n\nexport const {\n signIn,\n signUp,\n signOut,\n useSession,\n getSession,\n} = authClient\n",
@@ -21,6 +21,8 @@ import {
21
21
  SidebarMenu,
22
22
  SidebarMenuButton,
23
23
  SidebarMenuItem,
24
+ SidebarRail,
25
+ useSidebar,
24
26
  } from '@/components/ui/sidebar'
25
27
  import {
26
28
  DropdownMenu,
@@ -47,6 +49,7 @@ const navigation = [
47
49
  export function AppSidebar() {
48
50
  const router = useRouter()
49
51
  const { data: session } = useSession()
52
+ const { isMobile } = useSidebar()
50
53
  const user = session?.user
51
54
 
52
55
  const handleSignOut = async () => {
@@ -65,18 +68,18 @@ export function AppSidebar() {
65
68
  }
66
69
 
67
70
  return (
68
- <Sidebar>
71
+ <Sidebar collapsible="icon">
69
72
  <SidebarHeader>
70
73
  <SidebarMenu>
71
74
  <SidebarMenuItem>
72
- <SidebarMenuButton size="lg" asChild>
75
+ <SidebarMenuButton size="lg" asChild tooltip="{{projectName}}">
73
76
  <a href="/">
74
- <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
77
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
75
78
  <GalleryVerticalEnd className="size-4" />
76
79
  </div>
77
- <div className="flex flex-col gap-0.5 leading-none">
78
- <span className="font-semibold">{{projectName}}</span>
79
- <span className="text-xs text-muted-foreground">Dashboard</span>
80
+ <div className="grid flex-1 text-left text-sm leading-tight">
81
+ <span className="truncate font-semibold">{{projectName}}</span>
82
+ <span className="truncate text-xs">Dashboard</span>
80
83
  </div>
81
84
  </a>
82
85
  </SidebarMenuButton>
@@ -90,9 +93,9 @@ export function AppSidebar() {
90
93
  <SidebarMenu>
91
94
  {navigation.map((item) => (
92
95
  <SidebarMenuItem key={item.title}>
93
- <SidebarMenuButton asChild>
96
+ <SidebarMenuButton asChild tooltip={item.title}>
94
97
  <a href={item.url}>
95
- <item.icon className="size-4" />
98
+ <item.icon />
96
99
  <span>{item.title}</span>
97
100
  </a>
98
101
  </SidebarMenuButton>
@@ -119,7 +122,7 @@ export function AppSidebar() {
119
122
  </Avatar>
120
123
  <div className="grid flex-1 text-left text-sm leading-tight">
121
124
  <span className="truncate font-semibold">{user?.name ?? 'User'}</span>
122
- <span className="truncate text-xs text-muted-foreground">
125
+ <span className="truncate text-xs">
123
126
  {user?.email ?? ''}
124
127
  </span>
125
128
  </div>
@@ -127,26 +130,26 @@ export function AppSidebar() {
127
130
  </SidebarMenuButton>
128
131
  </DropdownMenuTrigger>
129
132
  <DropdownMenuContent
130
- className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
131
- side="bottom"
133
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
134
+ side={isMobile ? 'bottom' : 'right'}
132
135
  align="end"
133
136
  sideOffset={4}
134
137
  >
135
138
  <DropdownMenuItem asChild>
136
139
  <a href="/settings">
137
- <User className="mr-2 size-4" />
140
+ <User />
138
141
  Profile
139
142
  </a>
140
143
  </DropdownMenuItem>
141
144
  <DropdownMenuItem asChild>
142
145
  <a href="/settings">
143
- <Settings className="mr-2 size-4" />
146
+ <Settings />
144
147
  Settings
145
148
  </a>
146
149
  </DropdownMenuItem>
147
150
  <DropdownMenuSeparator />
148
151
  <DropdownMenuItem onClick={handleSignOut}>
149
- <LogOut className="mr-2 size-4" />
152
+ <LogOut />
150
153
  Sign out
151
154
  </DropdownMenuItem>
152
155
  </DropdownMenuContent>
@@ -154,6 +157,7 @@ export function AppSidebar() {
154
157
  </SidebarMenuItem>
155
158
  </SidebarMenu>
156
159
  </SidebarFooter>
160
+ <SidebarRail />
157
161
  </Sidebar>
158
162
  )
159
163
  }
@@ -20,18 +20,20 @@ export function DashboardLayout({ children, title = 'Dashboard' }: DashboardLayo
20
20
  <SidebarProvider>
21
21
  <AppSidebar />
22
22
  <SidebarInset>
23
- <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
24
- <SidebarTrigger className="-ml-1" />
25
- <Separator orientation="vertical" className="mr-2 h-4" />
26
- <Breadcrumb>
27
- <BreadcrumbList>
28
- <BreadcrumbItem>
29
- <BreadcrumbPage>{title}</BreadcrumbPage>
30
- </BreadcrumbItem>
31
- </BreadcrumbList>
32
- </Breadcrumb>
23
+ <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
24
+ <div className="flex items-center gap-2 px-4">
25
+ <SidebarTrigger className="-ml-1" />
26
+ <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
27
+ <Breadcrumb>
28
+ <BreadcrumbList>
29
+ <BreadcrumbItem>
30
+ <BreadcrumbPage>{title}</BreadcrumbPage>
31
+ </BreadcrumbItem>
32
+ </BreadcrumbList>
33
+ </Breadcrumb>
34
+ </div>
33
35
  </header>
34
- <main className="flex-1 p-4 md:p-6">{children}</main>
36
+ <main className="flex flex-1 flex-col gap-4 p-4 pt-0">{children}</main>
35
37
  </SidebarInset>
36
38
  </SidebarProvider>
37
39
  )