feat(web): scaffold Next.js 16 dashboard with design system and auth client (#82)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #82.
This commit is contained in:
11
apps/web/src/app/(auth)/layout.tsx
Normal file
11
apps/web/src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function AuthLayout({ children }: { children: ReactNode }): React.ReactElement {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-surface-bg">
|
||||
<div className="w-full max-w-md rounded-xl border border-surface-border bg-surface-card p-8 shadow-lg">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
apps/web/src/app/(auth)/login/page.tsx
Normal file
58
apps/web/src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function LoginPage(): React.ReactElement {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Sign in</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Sign in to your Mosaic account</p>
|
||||
|
||||
<form className="mt-6 space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-text-secondary">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-text-secondary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-surface-card"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-sm text-text-muted">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/register" className="text-blue-400 hover:text-blue-300">
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
apps/web/src/app/(auth)/register/page.tsx
Normal file
73
apps/web/src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function RegisterPage(): React.ReactElement {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Create account</h1>
|
||||
<p className="mt-1 text-sm text-text-secondary">Get started with Mosaic</p>
|
||||
|
||||
<form className="mt-6 space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-text-secondary">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
required
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-text-secondary">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-text-secondary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className="mt-1 block w-full rounded-lg border border-surface-border bg-surface-elevated px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-surface-card"
|
||||
>
|
||||
Create account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="mt-4 text-center text-sm text-text-muted">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="text-blue-400 hover:text-blue-300">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
apps/web/src/app/(dashboard)/chat/page.tsx
Normal file
8
apps/web/src/app/(dashboard)/chat/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function ChatPage(): React.ReactElement {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Chat</h1>
|
||||
<p className="mt-2 text-text-secondary">Conversations will appear here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
apps/web/src/app/(dashboard)/layout.tsx
Normal file
6
apps/web/src/app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { AppShell } from '@/components/layout/app-shell';
|
||||
|
||||
export default function DashboardLayout({ children }: { children: ReactNode }): React.ReactElement {
|
||||
return <AppShell>{children}</AppShell>;
|
||||
}
|
||||
115
apps/web/src/app/globals.css
Normal file
115
apps/web/src/app/globals.css
Normal file
@@ -0,0 +1,115 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/*
|
||||
* Mosaic Stack design tokens mapped to Tailwind v4 theme.
|
||||
* Source: @mosaic/design-tokens (AD-13)
|
||||
* Fonts: Outfit (sans), Fira Code (mono)
|
||||
* Palette: deep blue-grays + blue/purple/teal accents
|
||||
* Default: dark theme
|
||||
*/
|
||||
|
||||
@theme {
|
||||
/* ─── Fonts ─── */
|
||||
--font-sans: 'Outfit', system-ui, -apple-system, sans-serif;
|
||||
--font-mono: 'Fira Code', ui-monospace, Menlo, monospace;
|
||||
|
||||
/* ─── Neutral blue-gray scale ─── */
|
||||
--color-gray-50: #f0f2f5;
|
||||
--color-gray-100: #dce0e8;
|
||||
--color-gray-200: #b8c0cc;
|
||||
--color-gray-300: #8e99a9;
|
||||
--color-gray-400: #6b7a8d;
|
||||
--color-gray-500: #4e5d70;
|
||||
--color-gray-600: #3b4859;
|
||||
--color-gray-700: #2a3544;
|
||||
--color-gray-800: #1c2433;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-950: #0a0f1a;
|
||||
|
||||
/* ─── Primary — blue ─── */
|
||||
--color-blue-50: #eff4ff;
|
||||
--color-blue-100: #dae5ff;
|
||||
--color-blue-200: #bdd1ff;
|
||||
--color-blue-300: #8fb4ff;
|
||||
--color-blue-400: #5b8bff;
|
||||
--color-blue-500: #3b6cf7;
|
||||
--color-blue-600: #2551e0;
|
||||
--color-blue-700: #1d40c0;
|
||||
--color-blue-800: #1e369c;
|
||||
--color-blue-900: #1e317b;
|
||||
--color-blue-950: #162050;
|
||||
|
||||
/* ─── Accent — purple ─── */
|
||||
--color-purple-50: #f3f0ff;
|
||||
--color-purple-100: #e7dfff;
|
||||
--color-purple-200: #d2c3ff;
|
||||
--color-purple-300: #b49aff;
|
||||
--color-purple-400: #9466ff;
|
||||
--color-purple-500: #7c3aed;
|
||||
--color-purple-600: #6d28d9;
|
||||
--color-purple-700: #5b21b6;
|
||||
--color-purple-800: #4c1d95;
|
||||
--color-purple-900: #3b1578;
|
||||
--color-purple-950: #230d4d;
|
||||
|
||||
/* ─── Accent — teal ─── */
|
||||
--color-teal-50: #effcf9;
|
||||
--color-teal-100: #d0f7ef;
|
||||
--color-teal-200: #a4eddf;
|
||||
--color-teal-300: #6fddcb;
|
||||
--color-teal-400: #3ec5b2;
|
||||
--color-teal-500: #25aa99;
|
||||
--color-teal-600: #1c897e;
|
||||
--color-teal-700: #1b6e66;
|
||||
--color-teal-800: #1a5853;
|
||||
--color-teal-900: #194945;
|
||||
--color-teal-950: #082d2b;
|
||||
|
||||
/* ─── Semantic surface tokens ─── */
|
||||
--color-surface-bg: #0a0f1a;
|
||||
--color-surface-card: #111827;
|
||||
--color-surface-elevated: #1c2433;
|
||||
--color-surface-border: #2a3544;
|
||||
|
||||
/* ─── Semantic text tokens ─── */
|
||||
--color-text-primary: #f0f2f5;
|
||||
--color-text-secondary: #8e99a9;
|
||||
--color-text-muted: #6b7a8d;
|
||||
|
||||
/* ─── Status colors ─── */
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
--color-info: #3b82f6;
|
||||
|
||||
/* ─── Sidebar width ─── */
|
||||
--spacing-sidebar: 16rem;
|
||||
}
|
||||
|
||||
/* ─── Base styles ─── */
|
||||
body {
|
||||
background-color: var(--color-surface-bg);
|
||||
color: var(--color-text-primary);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ─── Scrollbar styling ─── */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-600);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray-500);
|
||||
}
|
||||
@@ -1,13 +1,29 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Outfit, Fira_Code } from 'next/font/google';
|
||||
import './globals.css';
|
||||
|
||||
const outfit = Outfit({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-sans',
|
||||
display: 'swap',
|
||||
weight: ['300', '400', '500', '600', '700'],
|
||||
});
|
||||
|
||||
const firaCode = Fira_Code({
|
||||
subsets: ['latin'],
|
||||
variable: '--font-mono',
|
||||
display: 'swap',
|
||||
weight: ['400', '500', '700'],
|
||||
});
|
||||
|
||||
export const metadata = {
|
||||
title: 'Mosaic',
|
||||
description: 'Mosaic Stack Dashboard',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }): ReactNode {
|
||||
export default function RootLayout({ children }: { children: ReactNode }): React.ReactElement {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className={`dark ${outfit.variable} ${firaCode.variable}`}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export default function HomePage(): React.ReactElement {
|
||||
return (
|
||||
<main>
|
||||
<h1>Mosaic Stack</h1>
|
||||
</main>
|
||||
);
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function HomePage(): never {
|
||||
redirect('/chat');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user