diff --git a/AGENTS.md b/AGENTS.md index c3a8d6b..0cbd7b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -179,3 +179,13 @@ Check with `claude mcp list`. - Keep contexts in `/src/lib/contexts/` folder, hooks in `/src/hooks/` folder, and utils in `/src/lib/utils/` folder. - Extract helper functions with no component dependencies to `/src/lib/utils/` or `/src/lib/` subfolders. + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/bun.lock b/bun.lock index 4bdb033..a7a50de 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "better-auth": "1.6.29", "class-variance-authority": "0.7.1", "clsx": "2.1.1", + "cmdk": "1.1.1", "idb-keyval": "6.2.6", "ioredis": "5.11.1", "javascript-time-ago": "2.6.4", @@ -1100,6 +1101,8 @@ "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], "color": ["color@5.0.2", "", { "dependencies": { "color-convert": "^3.0.1", "color-string": "^2.0.0" } }, "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA=="], diff --git a/package.json b/package.json index ea353c0..187ccf1 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "better-auth": "1.6.29", "class-variance-authority": "0.7.1", "clsx": "2.1.1", + "cmdk": "1.1.1", "idb-keyval": "6.2.6", "ioredis": "5.11.1", "javascript-time-ago": "2.6.4", diff --git a/scripts/removers/auth.ts b/scripts/removers/auth.ts index 77f72f3..7adad59 100644 --- a/scripts/removers/auth.ts +++ b/scripts/removers/auth.ts @@ -14,7 +14,6 @@ const AUTH_FILES_TO_DELETE = [ "src/lib/auth-client.ts", "src/lib/turnstile.ts", "src/hooks/use-toast.tsx", - "src/components/top-menu.tsx", "src/proxy.ts", "tests/turnstile.test.ts", ]; @@ -33,6 +32,7 @@ const AUTH_FOLDERS_TO_DELETE = [ const AUTH_FILES_TO_MODIFY = [ "src/app/(public)/layout.tsx", + "src/components/bottom-navigation.tsx", "src/lib/config.ts", "src/lib/public-config.ts", "src/lib/utils.ts", diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx index 9e08bf0..032b3fe 100644 --- a/src/app/(protected)/layout.tsx +++ b/src/app/(protected)/layout.tsx @@ -2,10 +2,11 @@ import type { Metadata } from "next"; import type React from "react"; import { Suspense } from "react"; import { SessionGate } from "@/components/auth/session-gate"; +import BottomNavigation from "@/components/bottom-navigation"; +import { CommandPalette } from "@/components/command-palette/command-palette"; import { AppSidebar } from "@/components/sidebar/app-sidebar"; -import { AppSidebarSkeleton } from "@/components/sidebar/app-sidebar-skeleton"; -import TopMenu from "@/components/top-menu"; import { SidebarProvider } from "@/components/ui/sidebar"; +import { CommandPaletteProvider } from "@/lib/contexts/command-palette-context"; export const metadata: Metadata = { robots: { index: false, follow: false }, @@ -17,19 +18,18 @@ export default function Layout({ children: React.ReactNode; }>) { return ( - - - - - }> + + + + + - -
-
- - {children} -
-
-
+
+
{children}
+
+ + + + ); } diff --git a/src/app/(protected)/not-found.tsx b/src/app/(protected)/not-found.tsx new file mode 100644 index 0000000..a6aca55 --- /dev/null +++ b/src/app/(protected)/not-found.tsx @@ -0,0 +1,20 @@ +import Link from "next/link"; +import { Button } from "@/components/ui/button"; + +export default function NotFound() { + return ( +
+
+

This page is not here

+

+ It may have been deleted, or the link may be out of date. Nothing is broken. +

+
+ +
+
+
+ ); +} diff --git a/src/app/(protected)/settings/page.tsx b/src/app/(protected)/settings/page.tsx index 3001bda..bce4f0e 100644 --- a/src/app/(protected)/settings/page.tsx +++ b/src/app/(protected)/settings/page.tsx @@ -13,40 +13,43 @@ import ApiKeysSkeleton from "@/components/settings/api-keys-skeleton"; // @catalyst:mcp-end import DeleteAccountForm from "@/components/settings/delete-account-form"; import PasswordForm from "@/components/settings/password-form"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { userHasPassword } from "@/lib/dao/users"; import { getUserIdFromSession } from "@/lib/session"; export default function SettingsPage() { return ( -
- - - Settings - Manage your account settings and preferences. - - -
- }> - - - {/* @catalyst:mcp-start */} - - }> - - - - {/* @catalyst:mcp-end */} - -
-
-
+
+
+

Settings

+

+ Change your password, or close your account. +

+
+ +
+

Password

+
+ }> + + +
+
+ + {/* @catalyst:mcp-start */} +
+

API keys

+ + }> + + + +
+ {/* @catalyst:mcp-end */} + +
+

Your account

+ +
); } diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx index 8363074..302d31d 100644 --- a/src/app/(public)/login/page.tsx +++ b/src/app/(public)/login/page.tsx @@ -23,10 +23,10 @@ export const metadata: Metadata = { export default function Login() { return ( -
+
+
diff --git a/src/app/(public)/reset-password/page.tsx b/src/app/(public)/reset-password/page.tsx index 1c2e167..b0b9c04 100644 --- a/src/app/(public)/reset-password/page.tsx +++ b/src/app/(public)/reset-password/page.tsx @@ -1,15 +1,9 @@ import type { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; +import AuthCard from "@/components/auth/auth-card"; import { PasswordResetFormSkeleton } from "@/components/auth/password-form-skeleton"; import PasswordResetForm from "@/components/auth/password-reset-form"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { openGraph } from "@/lib/metadata"; type SearchParams = Promise>; @@ -36,18 +30,16 @@ export const metadata: Metadata = { export default function Page({ searchParams }: Readonly<{ searchParams: SearchParams }>) { return ( -
- - - Reset Password - Enter your new password - - - }> - - - - +
+ + }> + + +
); } @@ -67,9 +59,9 @@ async function TokenBoundForm({ function InvalidResetLink() { return (

- This reset link is invalid or incomplete.{" "} - - Request a new one + This link is not valid any more.{" "} + + Ask for a new one .

diff --git a/src/app/(public)/reset-password/request/page.tsx b/src/app/(public)/reset-password/request/page.tsx index f66450f..413a6fa 100644 --- a/src/app/(public)/reset-password/request/page.tsx +++ b/src/app/(public)/reset-password/request/page.tsx @@ -1,12 +1,6 @@ import type { Metadata } from "next"; +import AuthCard from "@/components/auth/auth-card"; import RequestPasswordResetForm from "@/components/auth/request-password-reset-form"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { openGraph } from "@/lib/metadata"; const path = "/reset-password/request"; @@ -30,16 +24,14 @@ export const metadata: Metadata = { export default async function Page() { return ( -
- - - Reset Password - Enter your email to reset your password - - - - - +
+ + +
); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d64bd1d..f81756a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import type React from "react"; @@ -20,6 +20,15 @@ import { cn } from "@/lib/utils"; const inter = Inter({ subsets: ["latin"] }); +// Without resizes-content the layout viewport ignores the on-screen keyboard, so +// bottom-anchored sheets keep their full height behind it and push their fields +// off the top of the screen. +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + interactiveWidget: "resizes-content", +}; + export const metadata: Metadata = { metadataBase: new URL(siteUrl), title: metaTitle, diff --git a/src/components/auth/auth-card.tsx b/src/components/auth/auth-card.tsx index badb0fc..3fccd66 100644 --- a/src/components/auth/auth-card.tsx +++ b/src/components/auth/auth-card.tsx @@ -1,14 +1,6 @@ import Link from "next/link"; import type { ReactNode } from "react"; import OAuthForm from "@/components/auth/oauth-form"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import conf from "@/lib/config"; export default function AuthCard({ @@ -18,46 +10,55 @@ export default function AuthCard({ ctaQuestion, ctaText, ctaLink, + showOAuth = true, }: { title: string; description: string; children: ReactNode; - ctaQuestion: string; - ctaText: string; - ctaLink: string; + ctaQuestion?: string; + ctaText?: string; + ctaLink?: string; + showOAuth?: boolean; }) { const hasGitHub = Boolean(conf.githubId) && Boolean(conf.githubSecret); const hasGoogle = Boolean(conf.googleId) && Boolean(conf.googleSecret); + const oauth = showOAuth && (hasGitHub || hasGoogle); return ( - - - {title} - {description} - - - {children} - {(hasGitHub || hasGoogle) && ( +
+
+

{title}

+

{description}

+
+ + {children} + + {oauth && ( +
-
- Or continue with +
+ or continue with
- )} - {hasGoogle && } - {hasGitHub && } - - -

+ {hasGoogle && } + {hasGitHub && } +

+ )} + + {ctaQuestion && ctaText && ctaLink && ( +

{ctaQuestion}{" "} - + {ctaText}

- - + )} +
); } diff --git a/src/components/auth/auth-nav.tsx b/src/components/auth/auth-nav.tsx index 9429c3e..0afd2dc 100644 --- a/src/components/auth/auth-nav.tsx +++ b/src/components/auth/auth-nav.tsx @@ -19,14 +19,14 @@ export async function AuthNav() { return ( <> - Login + Log in ); } export function AuthNavSkeleton() { - return ; + return ; } diff --git a/src/components/auth/auth-submit-button.tsx b/src/components/auth/auth-submit-button.tsx new file mode 100644 index 0000000..18c182d --- /dev/null +++ b/src/components/auth/auth-submit-button.tsx @@ -0,0 +1,12 @@ +import LastUsedIndicator from "@/components/auth/last-used-indicator"; +import PendingSubmitButton from "@/components/pending-submit-button"; + +export default function AuthSubmitButton( + props: Readonly> +) { + return ( + + + + ); +} diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index 70887e6..60dd1b2 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -6,7 +6,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useActionState, useEffect, useId, useRef, useState, useTransition } from "react"; import { useForm } from "react-hook-form"; -import PendingSubmitButton from "@/components/auth/pending-submit-button"; +import AuthSubmitButton from "@/components/auth/auth-submit-button"; import TurnstileComponent from "@/components/auth/turnstile"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -99,7 +99,7 @@ export default function LoginForm() { href="/reset-password/request" className="text-primary text-xs underline-offset-4 hover:underline" > - Forgot Password? + Forgot password?
- - - + +
); } @@ -18,7 +18,7 @@ export function PasswordResetFormSkeleton() { {RESET_FIELDS.map((field) => ( ))} - + ); } @@ -29,7 +29,7 @@ export function PasswordFormSkeleton() { {ACCOUNT_FIELDS.map((field) => ( ))} - + ); } diff --git a/src/components/auth/password-reset-form.tsx b/src/components/auth/password-reset-form.tsx index 33414a9..2410d34 100644 --- a/src/components/auth/password-reset-form.tsx +++ b/src/components/auth/password-reset-form.tsx @@ -43,7 +43,7 @@ export default function PasswordResetForm({ token }: { token: string }) { return (
- +
- +
); diff --git a/src/components/auth/register-form.tsx b/src/components/auth/register-form.tsx index a7ddecc..ae52578 100644 --- a/src/components/auth/register-form.tsx +++ b/src/components/auth/register-form.tsx @@ -6,7 +6,7 @@ import { ExternalLink } from "lucide-react"; import { useRouter } from "next/navigation"; import { useActionState, useEffect, useId, useRef, useState, useTransition } from "react"; import { useForm } from "react-hook-form"; -import PendingSubmitButton from "@/components/auth/pending-submit-button"; +import AuthSubmitButton from "@/components/auth/auth-submit-button"; import TurnstileComponent from "@/components/auth/turnstile"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -145,7 +145,7 @@ export default function RegisterForm() { -
- Back to Login + Back to log in
diff --git a/src/components/bottom-nav-item.tsx b/src/components/bottom-nav-item.tsx new file mode 100644 index 0000000..2f30f48 --- /dev/null +++ b/src/components/bottom-nav-item.tsx @@ -0,0 +1,67 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { type ReactNode, Suspense } from "react"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +type BottomNavItemProps = { + href: string; + icon: ReactNode; + label: string; + badge?: ReactNode; +}; + +function BottomNavItemView({ + href, + icon, + label, + badge, + active, +}: BottomNavItemProps & { active: boolean }) { + return ( + + {active ? ( +