Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
21cd174
fix(layout): let the viewport resize around the on-screen keyboard
kovrichard Aug 19, 2026
557ec3c
fix(ui): raise mobile touch targets to 44px
kovrichard Aug 19, 2026
9c336a7
fix(ui): keep a tall drawer's footer reachable
kovrichard Aug 19, 2026
12d1609
fix(ui): render tooltips as a plain pill
kovrichard Aug 19, 2026
784d18e
fix(sidebar): keep server and client markup identical
kovrichard Aug 19, 2026
07ceeab
feat(sidebar): give the rail a handle that shows where it will move
kovrichard Aug 19, 2026
92a54f3
perf(sidebar): render the static shell outside the session boundary
kovrichard Aug 19, 2026
5e8ae55
fix(auth): size skeletons to the elements they stand in for
kovrichard Aug 19, 2026
eb58728
fix(auth): give the sign-in surfaces a consistent card
kovrichard Aug 19, 2026
842754d
refactor(copy): settle on "log in" and "sign up" across auth
kovrichard Aug 19, 2026
ece0b9a
fix(settings): lay the page out in the app shell
kovrichard Aug 19, 2026
bf66d47
feat(app): show a missing page inside the app shell
kovrichard Aug 19, 2026
2b059fa
feat(ui): add a native select primitive
kovrichard Aug 19, 2026
2a18b05
refactor(ui): make the pending submit button reusable outside auth
kovrichard Aug 19, 2026
c01cdac
feat(sidebar): give the sidebar a dashboard destination
kovrichard Aug 19, 2026
fa28316
feat(mobile): reach the app's destinations from a bottom bar
kovrichard Aug 19, 2026
b8e6387
fix(sidebar): keep the logo legible when the sidebar collapses
kovrichard Aug 19, 2026
6225536
feat(mobile): reach your profile from the bottom bar
kovrichard Aug 19, 2026
7c95869
fix(notifications): keep the bell in the shell and stream only its count
kovrichard Aug 19, 2026
b560212
feat(palette): open a command palette from the sidebar search
kovrichard Aug 20, 2026
bb8122d
docs(agents): carry the agent-rules block next dev writes
kovrichard Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- BEGIN:nextjs-agent-rules -->

# 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.

<!-- END:nextjs-agent-rules -->
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion scripts/removers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];
Expand All @@ -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",
Expand Down
30 changes: 15 additions & 15 deletions src/app/(protected)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -17,19 +18,18 @@ export default function Layout({
children: React.ReactNode;
}>) {
return (
<SidebarProvider>
<Suspense fallback={null}>
<SessionGate />
</Suspense>
<Suspense fallback={<AppSidebarSkeleton />}>
<CommandPaletteProvider>
<SidebarProvider>
<Suspense fallback={null}>
<SessionGate />
</Suspense>
<AppSidebar />
</Suspense>
<main className="relative flex min-h-screen flex-1 bg-muted/40">
<div className="flex flex-1 flex-col">
<TopMenu />
{children}
</div>
</main>
</SidebarProvider>
<main className="relative flex min-h-screen flex-1 bg-muted/40">
<div className="flex flex-1 flex-col pb-20 md:pb-0">{children}</div>
</main>
<BottomNavigation />
<CommandPalette />
</SidebarProvider>
</CommandPaletteProvider>
);
}
20 changes: 20 additions & 0 deletions src/app/(protected)/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";

export default function NotFound() {
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 p-4 sm:gap-8 sm:p-10">
<div className="flex flex-col gap-3 rounded-2xl border bg-card p-4 sm:p-5">
<h1 className="font-semibold text-2xl tracking-tight">This page is not here</h1>
<p className="text-muted-foreground text-sm">
It may have been deleted, or the link may be out of date. Nothing is broken.
</p>
<div className="flex flex-wrap gap-3 pt-1">
<Button asChild>
<Link href="/dashboard">Go home</Link>
</Button>
</div>
</div>
</div>
);
}
61 changes: 32 additions & 29 deletions src/app/(protected)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="relative flex max-h-svh min-w-[320px] flex-1 flex-col items-center justify-center bg-background">
<Card className="w-full max-w-md p-4">
<CardHeader>
<CardTitle>Settings</CardTitle>
<CardDescription>Manage your account settings and preferences.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<Suspense fallback={<PasswordFormSkeleton />}>
<AccountPasswordForm />
</Suspense>
{/* @catalyst:mcp-start */}
<ApiKeysCard>
<Suspense fallback={<ApiKeysSkeleton />}>
<ApiKeys />
</Suspense>
</ApiKeysCard>
{/* @catalyst:mcp-end */}
<DeleteAccountForm />
</div>
</CardContent>
</Card>
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 p-4 sm:gap-8 sm:p-10">
<div className="flex flex-col gap-1">
<h1 className="font-semibold text-2xl tracking-tight">Settings</h1>
<p className="text-muted-foreground text-sm">
Change your password, or close your account.
</p>
</div>

<section className="flex flex-col gap-3">
<h2 className="font-semibold">Password</h2>
<div className="rounded-2xl border bg-card p-4 sm:p-5">
<Suspense fallback={<PasswordFormSkeleton />}>
<AccountPasswordForm />
</Suspense>
</div>
</section>

{/* @catalyst:mcp-start */}
<section className="flex flex-col gap-3">
<h2 className="font-semibold">API keys</h2>
<ApiKeysCard>
<Suspense fallback={<ApiKeysSkeleton />}>
<ApiKeys />
</Suspense>
</ApiKeysCard>
</section>
{/* @catalyst:mcp-end */}

<section className="flex flex-col gap-3">
<h2 className="font-semibold">Your account</h2>
<DeleteAccountForm />
</section>
</div>
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/app/(public)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export const metadata: Metadata = {

export default function Login() {
return (
<main className="m-auto">
<main className="m-auto w-full px-4 py-8">
<AuthCard
title="Welcome back!"
description="Sign in to your account to continue"
title="Welcome back"
description="Log in to pick up where you left off."
ctaQuestion="First time here?"
ctaText="Sign up"
ctaLink="/register"
Expand Down
8 changes: 4 additions & 4 deletions src/app/(public)/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export const metadata: Metadata = {

export default function Login() {
return (
<main className="m-auto">
<main className="m-auto w-full px-4 py-8">
<AuthCard
title="Let's get started!"
description="Create an account to continue"
title="Create your account"
description="It takes about a minute."
ctaQuestion="Already have an account?"
ctaText="Login"
ctaText="Log in"
ctaLink="/login"
>
<RegisterForm />
Expand Down
36 changes: 14 additions & 22 deletions src/app/(public)/reset-password/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, string | string[] | undefined>>;
Expand All @@ -36,18 +30,16 @@ export const metadata: Metadata = {

export default function Page({ searchParams }: Readonly<{ searchParams: SearchParams }>) {
return (
<main className="m-auto">
<Card className="w-92">
<CardHeader className="text-center">
<CardTitle>Reset Password</CardTitle>
<CardDescription>Enter your new password</CardDescription>
</CardHeader>
<CardContent>
<Suspense fallback={<PasswordResetFormSkeleton />}>
<TokenBoundForm searchParams={searchParams} />
</Suspense>
</CardContent>
</Card>
<main className="m-auto w-full px-4 py-8">
<AuthCard
title="Choose a new password"
description="Pick something you have not used here before."
showOAuth={false}
>
<Suspense fallback={<PasswordResetFormSkeleton />}>
<TokenBoundForm searchParams={searchParams} />
</Suspense>
</AuthCard>
</main>
);
}
Expand All @@ -67,9 +59,9 @@ async function TokenBoundForm({
function InvalidResetLink() {
return (
<p className="text-center text-muted-foreground text-sm">
This reset link is invalid or incomplete.{" "}
<Link href="/reset-password/request" className="underline">
Request a new one
This link is not valid any more.{" "}
<Link href="/reset-password/request" className="underline underline-offset-4">
Ask for a new one
</Link>
.
</p>
Expand Down
26 changes: 9 additions & 17 deletions src/app/(public)/reset-password/request/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -30,16 +24,14 @@ export const metadata: Metadata = {

export default async function Page() {
return (
<main className="m-auto">
<Card className="w-92">
<CardHeader className="text-center">
<CardTitle>Reset Password</CardTitle>
<CardDescription>Enter your email to reset your password</CardDescription>
</CardHeader>
<CardContent>
<RequestPasswordResetForm />
</CardContent>
</Card>
<main className="m-auto w-full px-4 py-8">
<AuthCard
title="Forgot your password?"
description="We will email you a link to set a new one."
showOAuth={false}
>
<RequestPasswordResetForm />
</AuthCard>
</main>
);
}
11 changes: 10 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down
Loading
Loading