Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 119 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# BrandKit Design System

## Overview

BrandKit uses a warm editorial marketing system inspired by Intercom's cream canvas. The page ground is not pure white: it is a soft cream canvas (`#f5f1ec`) that signals calm, product focus, and launch readiness. White floating cards sit on that canvas with thin warm hairline borders, while charcoal (`#111111`) carries headings, body text, and primary actions.

The product should feel useful before it feels decorative. Public pages lead with high-fidelity product or asset mockups framed in white cards. Marketing chrome stays quiet so favicon, OG image, app icon, and code-snippet outputs become the protagonist.

## Color Tokens

### Brand

- `colors.ink`: `#111111` — system primary for headlines, body text, and primary CTAs.
- `colors.on-primary`: `#fffdf9` — text on charcoal CTAs.
- `colors.fin-orange`: `#ff5600` — AI/product accent, reserved for AI CTA badges and a few inline emphasis moments.
- `colors.brand-blue`: `#0007cb` — available only inside product mockups or asset previews.

### Surfaces

- `colors.canvas`: `#f5f1ec` — default page background.
- `colors.surface-1`: `#ffffff` — floating cards, pricing cards, mockup cards, dialogs.
- `colors.surface-2`: `#ede8df` — secondary panels, nav hover, alternate rows.
- `colors.hairline`: `#d3cec6` — card borders and structural dividers.
- `colors.hairline-soft`: `#e2ddd4` — FAQ/footer subtle dividers.
- `colors.inverse-canvas`: `#000000` — testimonial/quote strips only.

### Text

- `colors.ink-muted`: `#626260` — secondary text.
- `colors.ink-subtle`: `#7b7b78` — tertiary text.
- `colors.ink-tertiary`: `#9c9fa5` — disabled/help text.
- `colors.inverse-ink`: `#fffdf9` — text on inverse canvas.
- `colors.inverse-ink-muted`: `#c7c3bc` — secondary text on inverse canvas.

### Product Mockup Palette

Report colors are allowed inside mockups only: blue `#0007cb`, green `#11a36a`, pink `#f45b99`, lime `#c7f464`, cyan `#39c5cf`, orange `#ff7a1a`.

## Typography

Use one geometric sans family throughout. If Saans is unavailable, use Inter or Geist Sans. Body and display stay in the same family; hierarchy comes from size, weight, line height, and letter spacing.

- Display XL: 72px, 500, line-height 1.05, letter-spacing -2px.
- Display LG: 56px, 500, line-height 1.10, letter-spacing -1.4px.
- Display MD: 40px, 500, line-height 1.15, letter-spacing -0.8px.
- Headline: 28px, 500, line-height 1.20, letter-spacing -0.5px.
- Card title: 22px, 500, line-height 1.25, letter-spacing -0.3px.
- Body LG: 18px, 400, line-height 1.50.
- Body: 16px, 400, line-height 1.50.
- Caption: 12px, 400, line-height 1.40.
- Button: 15px, 500, line-height 1.20.
- Mono: 13px, 400, line-height 1.50. Use only for code snippets or product UI details.

Do not use all-caps tracked eyebrows. Section labels use sentence case, 14px, weight 500.

## Layout

- Base spacing unit: 8px.
- Maximum content width: 1280px.
- Section rhythm: 96px vertical spacing on desktop.
- Card grids: 3-up desktop, 2-up tablet, 1-up mobile.
- Product mockup cards span wide and maintain aspect ratio without cropping.
- The cream canvas creates separation; avoid colorful section backgrounds.

## Components

### Buttons

- Primary: charcoal background, cream/white text, 8px radius, 10px vertical and 18px horizontal padding.
- Secondary: white background, charcoal text, warm hairline border, 8px radius.
- Tertiary: cream background, charcoal text, no decorative border.
- Orange CTA: `#ff5600`, reserved for AI/product moments only.

Do not pill-round CTAs. Do not combine charcoal and orange primary CTAs in the same viewport.

### Cards

- Standard card: white background, `1px` warm hairline border, 12px radius, no drop shadow.
- Product mockup card: white background, hairline border, 16px radius, 24px padding.
- CTA banner: white background, hairline border, 12px radius, 48px padding.
- Featured pricing card: charcoal background, cream text, 12px radius.

Depth comes from white-on-cream contrast, not shadows or glows.

### Forms

Inputs use white background, charcoal text, warm hairline border, 8px radius, and a charcoal focus ring. Errors use semantic red, not orange.

### Navigation & Footer

Navigation and footer sit directly on the cream canvas. Nav height is compact, and links use muted charcoal until hover. The footer is a dense link grid, not a decorative block.

## Do

- Keep the cream canvas as the anchor surface.
- Lift important content onto white cards.
- Make product/asset previews the visual payload of each section.
- Use charcoal as the system primary.
- Use orange sparingly for AI/product emphasis.
- Keep card corners modest: 12px or 16px.
- Keep body text at normal tracking.

## Don't

- Do not use pure white as the page canvas.
- Do not use purple/blue gradients as the brand surface.
- Do not add atmospheric orbs, glow borders, or heavy drop shadows.
- Do not use Fin Orange as a generic system primary.
- Do not pill-round normal CTAs.
- Do not use mono type in marketing chrome.
- Do not promote report palette colors to page backgrounds.

## Responsive

- CTAs hold at least 40px tap height.
- Inputs hold at least 44px tap height.
- Product mockups maintain aspect ratio and do not crop.
- Display type scales from ~72px desktop toward ~40px tablet and ~32px mobile.
- Navigation collapses below 768px while preserving the primary path.
12 changes: 2 additions & 10 deletions app/[locale]/(auth)/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { getSafeRedirectUrl } from '@/lib/utils/url'

export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url)
Expand All @@ -11,16 +12,7 @@ export async function GET(request: NextRequest) {
const { error } = await supabase.auth.exchangeCodeForSession(code)

if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host')
const isLocalEnv = process.env.NODE_ENV === 'development'

if (isLocalEnv) {
return NextResponse.redirect(`${origin}${next}`)
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`)
} else {
return NextResponse.redirect(`${origin}${next}`)
}
return NextResponse.redirect(getSafeRedirectUrl(origin, next))
}
}

Expand Down
3 changes: 2 additions & 1 deletion app/[locale]/(auth)/auth/confirm/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type NextRequest } from 'next/server'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { type EmailOtpType } from '@supabase/supabase-js'
import { getSafeRedirectPath } from '@/lib/utils/url'

export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
Expand All @@ -14,7 +15,7 @@ export async function GET(request: NextRequest) {
const { error } = await supabase.auth.verifyOtp({ type, token_hash })

if (!error) {
redirect(next)
redirect(getSafeRedirectPath(next))
}
}

Expand Down
15 changes: 6 additions & 9 deletions app/[locale]/(auth)/forgot-password/forgot-password-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,21 @@ export default function ForgotPasswordPage() {
}

return (
<div className="flex min-h-screen">
<div className="flex min-h-screen bg-background">
{/* Left: Branding panel */}
<div className="bg-brand relative hidden w-1/2 overflow-hidden lg:block">
<div className="grid-pattern absolute inset-0" />
<div className="gradient-mesh-dark absolute inset-0" />
<div className="noise absolute inset-0" />
<div className="relative hidden w-1/2 overflow-hidden border-r border-border bg-background lg:block">
<div className="relative z-10 flex h-full flex-col justify-between p-12">
<Link
href="/"
className="animate-fade-in font-display text-xl font-bold tracking-tight text-white"
className="animate-fade-in font-display text-xl font-medium tracking-[-0.3px] text-text-primary"
>
{tCommon('brandName')}
</Link>
<div className="animate-fade-in-up delay-1">
<h2 className="font-display tracking-headline text-4xl leading-[0.95] font-bold text-white xl:text-5xl">
<h2 className="font-display text-4xl font-medium leading-[1.08] tracking-[-1px] text-text-primary xl:text-5xl">
{t('title')}
</h2>
<p className="mt-6 max-w-sm text-sm leading-relaxed text-white/50">
<p className="mt-6 max-w-sm text-sm leading-relaxed text-text-secondary">
{t('description')}
</p>
</div>
Expand All @@ -63,7 +60,7 @@ export default function ForgotPasswordPage() {

{/* Right: Form */}
<main id="main-content" className="flex flex-1 items-center justify-center px-6 py-12">
<Card className="w-full max-w-md border-0 shadow-none lg:border lg:shadow-sm">
<Card className="w-full max-w-md border-border">
<CardHeader className="lg:hidden">
<h2 className="font-display text-text-primary text-2xl">{t('title')}</h2>
<CardDescription>{t('description')}</CardDescription>
Expand Down
78 changes: 56 additions & 22 deletions app/[locale]/(auth)/login/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,78 @@ import { createClient } from '@/lib/supabase/server'
import { headers } from 'next/headers'
import { getBaseUrl } from '@/lib/utils/url'

export type AuthActionResult =
| { error?: string; errorCode?: 'AUTH_SERVICE_UNAVAILABLE' }
| undefined

function isAuthServiceUnavailable(error: unknown): boolean {
const message = error instanceof Error
? error.message
: typeof error === 'object' && error && 'message' in error
? String((error as { message?: unknown }).message)
: String(error)

return /fetch failed|failed to fetch|network|enotfound|eai_again/i.test(message)
}

export async function signIn({
email,
password,
}: {
email: string
password: string
}) {
const supabase = await createClient()
}): Promise<AuthActionResult> {
try {
const supabase = await createClient()

const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})

if (error) {
return { error: error.message }
if (error) {
if (isAuthServiceUnavailable(error)) {
return { errorCode: 'AUTH_SERVICE_UNAVAILABLE' }
}
return { error: error.message }
}
} catch (error) {
console.error('Email sign in failed:', error)
return { errorCode: 'AUTH_SERVICE_UNAVAILABLE' }
}

redirect('/dashboard')
}

export async function signInWithOAuth(provider: 'google' | 'github') {
const supabase = await createClient()
const headerStore = await headers()
const origin = headerStore.get('origin') || getBaseUrl()
export async function signInWithOAuth(provider: 'google' | 'github'): Promise<AuthActionResult> {
let redirectUrl: string | null = null

try {
const supabase = await createClient()
const headerStore = await headers()
const origin = headerStore.get('origin') || getBaseUrl()

const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${origin}/auth/callback`,
},
})

const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${origin}/auth/callback`,
},
})
if (error) {
if (isAuthServiceUnavailable(error)) {
return { errorCode: 'AUTH_SERVICE_UNAVAILABLE' }
}
return { error: error.message }
}

if (error) {
return { error: error.message }
redirectUrl = data.url ?? null
} catch (error) {
console.error(`${provider} sign in failed:`, error)
return { errorCode: 'AUTH_SERVICE_UNAVAILABLE' }
}

if (data.url) {
redirect(data.url)
if (redirectUrl) {
redirect(redirectUrl)
}
}
Loading
Loading