Compare commits

...

10 Commits

Author SHA1 Message Date
Cursor Agent
ba5d1575b3 fix: remove light-theme styles from chat auth components
- Remove white backgrounds and light focus rings from OTP inputs in email auth
- Remove light-theme focus styles from email and password inputs
- Update Label import to use @/components/emcn in password auth
- Maintain error state styling while removing light-theme overrides
2026-03-15 22:41:36 +00:00
waleed
a814db77b8 fix(ui): fallback to white button when whitelabeled without primary color 2026-03-15 15:33:48 -07:00
waleed
84b8604f7c fix(ui): add missing dark class to 404 page, add relative positioning to invite layout 2026-03-15 15:30:17 -07:00
waleed
4ede522671 fix(ui): address PR review comments - restore StatusPageLayout wrapper, improve whitelabel detection 2026-03-15 15:19:09 -07:00
waleed
0725c98afa improvement(ui): align all public pages with dark landing theme and improve whitelabeling 2026-03-15 07:44:16 -07:00
waleed
c191f15501 fix(sidebar): hoist skeleton section counts to module constant 2026-03-15 05:55:06 -07:00
waleed
03739d0352 fix(sidebar): match collapsed skeleton section grouping with loaded layout 2026-03-15 05:54:00 -07:00
waleed
a58de5a24b fix(sidebar): align collapsed settings skeletons with actual icon positions 2026-03-15 05:52:59 -07:00
waleed
af872df5f8 fix(sidebar): hide expanded skeletons during pre-hydration in collapsed settings 2026-03-15 05:50:59 -07:00
waleed
71afa3a87c fix(sidebar): show icon-sized skeletons in collapsed settings sidebar 2026-03-15 05:49:30 -07:00
74 changed files with 1056 additions and 1423 deletions

View File

@@ -2,32 +2,22 @@
import { useEffect } from 'react'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Nav from '@/app/(landing)/components/nav/nav'
function isColorDark(hexColor: string): boolean {
const hex = hexColor.replace('#', '')
const r = Number.parseInt(hex.substr(0, 2), 16)
const g = Number.parseInt(hex.substr(2, 2), 16)
const b = Number.parseInt(hex.substr(4, 2), 16)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance < 0.5
}
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function AuthLayoutClient({ children }: { children: React.ReactNode }) {
useEffect(() => {
const rootStyle = getComputedStyle(document.documentElement)
const brandBackground = rootStyle.getPropertyValue('--brand-background-hex').trim()
if (brandBackground && isColorDark(brandBackground)) {
document.body.classList.add('auth-dark-bg')
} else {
document.body.classList.remove('auth-dark-bg')
document.documentElement.classList.add('dark')
return () => {
document.documentElement.classList.remove('dark')
}
}, [])
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
<header className='shrink-0 bg-[#1C1C1C]'>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>{children}</div>
</div>

View File

@@ -1,44 +1,93 @@
export default function AuthBackgroundSVG() {
return (
<svg
aria-hidden='true'
className='pointer-events-none fixed inset-0 h-full w-full'
style={{ zIndex: 5 }}
viewBox='0 0 1880 960'
fill='none'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid slice'
>
{/* Right side paths - extended to connect */}
<path
d='M1393.53 42.8889C1545.99 173.087 1688.28 339.75 1878.44 817.6'
stroke='#E7E4EF'
strokeWidth='2'
/>
<path d='M1624.21 960L1625.78 0' stroke='#E7E4EF' strokeWidth='2' />
<path d='M1832.67 715.81L1880 716.031' stroke='#E7E4EF' strokeWidth='2' />
<path d='M1393.4 40V0' stroke='#E7E4EF' strokeWidth='2' />
<circle cx='1393.03' cy='40.0186' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
<circle cx='1625.28' cy='303.147' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
<circle cx='1837.37' cy='715.81' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
<>
{/* Top-left card outline */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-[-3vw] left-[-10vw] z-[5] aspect-[344/328] w-[38vw]'
>
<svg
viewBox='0 0 344 328'
fill='none'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid meet'
className='h-full w-full'
>
<path
d='M322.641 326.586L335.508 326.586C339.926 326.586 343.508 323.004 343.508 318.586V153.613C343.508 149.195 339.926 145.613 335.508 145.613H228.282C223.864 145.613 220.282 142.031 220.282 137.613V-50H190.282V137.613C190.282 142.031 186.7 145.613 182.282 145.613H-157V318.586C-157 323.004 -153.418 326.586 -149 326.586H322.641Z'
fill='#1C1C1C'
stroke='#323232'
strokeOpacity='0.4'
strokeWidth='1'
/>
</svg>
</div>
{/* Left side paths - extended to connect */}
<path
d='M160 157.764C319.811 136.451 417.278 102.619 552.39 0'
stroke='#E7E4EF'
strokeWidth='2'
/>
<path d='M310.22 803.025V0' stroke='#E7E4EF' strokeWidth='2' />
<path
d='M160 530.184C256.142 655.353 308.338 749.141 348.382 960'
stroke='#E7E4EF'
strokeWidth='2'
/>
<path d='M160 157.764V960' stroke='#E7E4EF' strokeWidth='2' />
<path d='M-50 157.764L160 157.764' stroke='#E7E4EF' strokeWidth='2' />
<circle cx='160' cy='157.764' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
<circle cx='310.22' cy='803.025' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
<circle cx='160' cy='530.184' r='8.07846' fill='white' stroke='#E7E4EF' strokeWidth='2' />
</svg>
{/* Top-right card outline */}
<div
aria-hidden='true'
className='pointer-events-none absolute top-[-4vw] right-[-14vw] z-[5] aspect-[471/470] w-[42vw]'
>
<svg
viewBox='0 0 471 470'
fill='none'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid meet'
className='h-full w-full'
>
<path
d='M471 94.274L471 124.274L365.88 124.274C361.462 124.274 357.88 127.856 357.88 132.274L357.88 225.495C357.88 229.913 354.298 233.495 349.88 233.495L219.5 233.495C215.082 233.495 211.5 237.077 211.5 241.495L211.5 461.5C211.5 465.918 207.918 469.5 203.5 469.5L8.5 469.5C4.082 469.5 0.5 465.918 0.5 461.5L0.5 157.274C0.5 152.856 4.082 149.274 8.5 149.274L184 149.274C188.418 149.274 192 145.692 192 141.274L192 102.274C192 97.856 195.582 94.274 200 94.274L471 94.274Z'
fill='#1C1C1C'
stroke='#323232'
strokeOpacity='0.4'
strokeWidth='1'
/>
</svg>
</div>
{/* Bottom-left card outline (mirrored) */}
<div
aria-hidden='true'
className='pointer-events-none absolute bottom-[-6vw] left-[-12vw] z-[5] aspect-[471/470] w-[36vw] rotate-180'
>
<svg
viewBox='0 0 471 470'
fill='none'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid meet'
className='h-full w-full'
>
<path
d='M471 94.274L471 124.274L365.88 124.274C361.462 124.274 357.88 127.856 357.88 132.274L357.88 225.495C357.88 229.913 354.298 233.495 349.88 233.495L219.5 233.495C215.082 233.495 211.5 237.077 211.5 241.495L211.5 461.5C211.5 465.918 207.918 469.5 203.5 469.5L8.5 469.5C4.082 469.5 0.5 465.918 0.5 461.5L0.5 157.274C0.5 152.856 4.082 149.274 8.5 149.274L184 149.274C188.418 149.274 192 145.692 192 141.274L192 102.274C192 97.856 195.582 94.274 200 94.274L471 94.274Z'
fill='#1C1C1C'
stroke='#323232'
strokeOpacity='0.4'
strokeWidth='1'
/>
</svg>
</div>
{/* Bottom-right card outline (mirrored) */}
<div
aria-hidden='true'
className='pointer-events-none absolute right-[-12vw] bottom-[-5vw] z-[5] aspect-[344/328] w-[34vw] rotate-180'
>
<svg
viewBox='0 0 344 328'
fill='none'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid meet'
className='h-full w-full'
>
<path
d='M322.641 326.586L335.508 326.586C339.926 326.586 343.508 323.004 343.508 318.586V153.613C343.508 149.195 339.926 145.613 335.508 145.613H228.282C223.864 145.613 220.282 142.031 220.282 137.613V-50H190.282V137.613C190.282 142.031 186.7 145.613 182.282 145.613H-157V318.586C-157 323.004 -153.418 326.586 -149 326.586H322.641Z'
fill='#1C1C1C'
stroke='#323232'
strokeOpacity='0.4'
strokeWidth='1'
/>
</svg>
</div>
</>
)
}

View File

@@ -8,10 +8,10 @@ type AuthBackgroundProps = {
export default function AuthBackground({ className, children }: AuthBackgroundProps) {
return (
<div className={cn('relative min-h-screen w-full overflow-hidden', className)}>
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
<div className={cn('fixed inset-0 overflow-hidden', className)}>
<div className='-z-50 pointer-events-none absolute inset-0 bg-[#1C1C1C]' />
<AuthBackgroundSVG />
<div className='relative z-20'>{children}</div>
<div className='relative z-20 h-full overflow-auto'>{children}</div>
</div>
)
}

View File

@@ -2,36 +2,20 @@
import { forwardRef, useState } from 'react'
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
import { Button, type ButtonProps as EmcnButtonProps } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
import { useBrandConfig } from '@/ee/whitelabeling'
export interface BrandedButtonProps extends Omit<EmcnButtonProps, 'variant' | 'size'> {
/** Shows loading spinner and disables button */
export interface BrandedButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
loading?: boolean
/** Text to show when loading (appends "..." automatically) */
loadingText?: string
/** Show arrow animation on hover (default: true) */
showArrow?: boolean
/** Make button full width (default: true) */
fullWidth?: boolean
}
/**
* Branded button for auth and status pages.
* Automatically detects whitelabel customization and applies appropriate styling.
*
* @example
* ```tsx
* // Primary branded button with arrow
* <BrandedButton onClick={handleSubmit}>Sign In</BrandedButton>
*
* // Loading state
* <BrandedButton loading loadingText="Signing in">Sign In</BrandedButton>
*
* // Without arrow animation
* <BrandedButton showArrow={false}>Continue</BrandedButton>
* ```
* Default: white button matching the landing page "Get started" style.
* Whitelabel: uses the brand's primary color as background with white text.
*/
export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
(
@@ -49,7 +33,8 @@ export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
},
ref
) => {
const buttonClass = useBrandedButtonClass()
const brand = useBrandConfig()
const hasCustomColor = brand.isWhitelabeled && Boolean(brand.theme?.primaryColor)
const [isHovered, setIsHovered] = useState(false)
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -63,14 +48,31 @@ export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
}
return (
<Button
<button
ref={ref}
variant='branded'
size='branded'
disabled={disabled || loading}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={cn(buttonClass, 'group', fullWidth && 'w-full', className)}
className={cn(
'group inline-flex h-[30px] items-center justify-center gap-[7px] rounded-[5px] border px-[9px] text-[13.5px] transition-colors disabled:cursor-not-allowed disabled:opacity-50',
!hasCustomColor &&
'border-[#FFFFFF] bg-[#FFFFFF] text-black hover:border-[#E0E0E0] hover:bg-[#E0E0E0]',
fullWidth && 'w-full',
className
)}
style={
hasCustomColor
? {
backgroundColor: isHovered
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
: brand.theme?.primaryColor,
borderColor: isHovered
? (brand.theme?.primaryHoverColor ?? brand.theme?.primaryColor)
: brand.theme?.primaryColor,
color: '#FFFFFF',
}
: undefined
}
{...props}
>
{loading ? (
@@ -92,7 +94,7 @@ export const BrandedButton = forwardRef<HTMLButtonElement, BrandedButtonProps>(
) : (
children
)}
</Button>
</button>
)
}
)

View File

@@ -1,10 +1,9 @@
'use client'
import { type ReactNode, useEffect, useState } from 'react'
import { Button } from '@/components/emcn'
import { GithubIcon, GoogleIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import { client } from '@/lib/auth/auth-client'
import { inter } from '@/app/_styles/fonts/inter/inter'
interface SocialLoginButtonsProps {
githubAvailable: boolean
@@ -82,7 +81,7 @@ export function SocialLoginButtons({
const githubButton = (
<Button
variant='outline'
className='w-full rounded-[10px] shadow-sm hover:bg-gray-50'
className='w-full rounded-[10px]'
disabled={!githubAvailable || isGithubLoading}
onClick={signInWithGithub}
>
@@ -94,7 +93,7 @@ export function SocialLoginButtons({
const googleButton = (
<Button
variant='outline'
className='w-full rounded-[10px] shadow-sm hover:bg-gray-50'
className='w-full rounded-[10px]'
disabled={!googleAvailable || isGoogleLoading}
onClick={signInWithGoogle}
>
@@ -110,7 +109,7 @@ export function SocialLoginButtons({
}
return (
<div className={`${inter.className} grid gap-3 font-light`}>
<div className='grid gap-3 font-light'>
{googleAvailable && googleButton}
{githubAvailable && githubButton}
{children}

View File

@@ -1,7 +1,7 @@
'use client'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Button } from '@/components/emcn'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
@@ -38,7 +38,7 @@ export function SSOLoginButton({
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
)
const outlineBtnClasses = cn('w-full rounded-[10px] shadow-sm hover:bg-gray-50')
const outlineBtnClasses = cn('w-full rounded-[10px]')
return (
<Button

View File

@@ -1,69 +1,37 @@
'use client'
import type { ReactNode } from 'react'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
import { SupportFooter } from './support-footer'
export interface StatusPageLayoutProps {
/** Page title displayed prominently */
title: string
/** Description text below the title */
description: string | ReactNode
/** Content to render below the title/description (usually buttons) */
children?: ReactNode
/** Whether to show the support footer (default: true) */
showSupportFooter?: boolean
/** Whether to hide the nav bar (useful for embedded forms) */
hideNav?: boolean
}
/**
* Unified layout for status/error pages (404, form unavailable, chat error, etc.).
* Uses AuthBackground and Nav for consistent styling with auth pages.
*
* @example
* ```tsx
* <StatusPageLayout
* title="Page Not Found"
* description="The page you're looking for doesn't exist."
* >
* <BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
* </StatusPageLayout>
* ```
*/
export function StatusPageLayout({
title,
description,
children,
showSupportFooter = true,
hideNav = false,
}: StatusPageLayoutProps) {
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
{!hideNav && <Nav hideAuthButtons={true} variant='auth' />}
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
<header className='shrink-0 bg-[#1C1C1C]'>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
>
{title}
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{description}
</p>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>{title}</h1>
<p className='font-[380] text-[#999] text-[16px]'>{description}</p>
</div>
{children && (
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
{children}
</div>
)}
{children && <div className='mt-8 w-full max-w-[410px] space-y-3'>{children}</div>}
</div>
</div>
</div>

View File

@@ -1,37 +1,22 @@
'use client'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { useBrandConfig } from '@/ee/whitelabeling'
export interface SupportFooterProps {
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */
position?: 'fixed' | 'absolute'
}
/**
* Support footer component for auth and status pages.
* Displays a "Need help? Contact support" link using branded support email.
*
* @example
* ```tsx
* // Fixed position (for standalone pages)
* <SupportFooter />
*
* // Absolute position (for pages using AuthLayout)
* <SupportFooter position="absolute" />
* ```
*/
export function SupportFooter({ position = 'fixed' }: SupportFooterProps) {
const brandConfig = useBrandConfig()
return (
<div
className={`${inter.className} auth-text-muted right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed ${position}`}
className={`right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed ${position}`}
>
Need help?{' '}
<a
href={`mailto:${brandConfig.supportEmail}`}
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Contact support
</a>

View File

@@ -6,21 +6,19 @@ import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
Input,
Label,
Modal,
ModalBody,
ModalContent,
ModalDescription,
ModalHeader,
} from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -385,17 +383,13 @@ export default function LoginPage({
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Sign in
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Enter your details
</p>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>Sign in</h1>
<p className='font-[380] text-[#999] text-[16px]'>Enter your details</p>
</div>
{/* SSO Login Button (primary top-only when it is the only method) */}
{showTopSSO && (
<div className={`${inter.className} mt-8`}>
<div className='mt-8'>
<SSOLoginButton
callbackURL={callbackUrl}
variant='primary'
@@ -406,14 +400,14 @@ export default function LoginPage({
{/* Password reset success message */}
{resetSuccessMessage && (
<div className={`${inter.className} mt-1 space-y-1 text-[#4CAF50] text-xs`}>
<div className='mt-1 space-y-1 text-[#4CAF50] text-xs'>
<p>{resetSuccessMessage}</p>
</div>
)}
{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
<form onSubmit={onSubmit} className='mt-8 space-y-8'>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -430,10 +424,9 @@ export default function LoginPage({
value={email}
onChange={handleEmailChange}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showEmailValidationError &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
/>
{showEmailValidationError && emailErrors.length > 0 && (
@@ -450,7 +443,7 @@ export default function LoginPage({
<button
type='button'
onClick={() => setForgotPasswordOpen(true)}
className='font-medium text-muted-foreground text-xs transition hover:text-foreground'
className='font-medium text-[#999] text-xs transition hover:text-[#ECECEC]'
>
Forgot password?
</button>
@@ -468,16 +461,16 @@ export default function LoginPage({
value={password}
onChange={handlePasswordChange}
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
'pr-10',
showValidationError &&
passwordErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -506,18 +499,18 @@ export default function LoginPage({
{/* Divider - show when we have multiple auth methods */}
{showDivider && (
<div className={`${inter.className} relative my-6 font-light`}>
<div className='relative my-6 font-light'>
<div className='absolute inset-0 flex items-center'>
<div className='auth-divider w-full border-t' />
<div className='w-full border-[#2A2A2A] border-t' />
</div>
<div className='relative flex justify-center text-sm'>
<span className='bg-white px-4 font-[340] text-muted-foreground'>Or continue with</span>
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
</div>
</div>
)}
{showBottomSection && (
<div className={cn(inter.className, !emailEnabled ? 'mt-8' : undefined)}>
<div className={cn(!emailEnabled ? 'mt-8' : undefined)}>
<SocialLoginButtons
googleAvailable={googleAvailable}
githubAvailable={githubAvailable}
@@ -537,26 +530,24 @@ export default function LoginPage({
{/* Only show signup link if email/password signup is enabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<div className='pt-6 text-center font-light text-[14px]'>
<span className='font-normal'>Don't have an account? </span>
<Link
href={isInviteFlow ? `/signup?invite_flow=true&callbackUrl=${callbackUrl}` : '/signup'}
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
>
Sign up
</Link>
</div>
)}
<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
>
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
By signing in, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Terms of Service
</Link>{' '}
@@ -565,64 +556,58 @@ export default function LoginPage({
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Privacy Policy
</Link>
</div>
<Dialog open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
<DialogContent className='auth-card auth-card-shadow max-w-[540px] rounded-[10px] border backdrop-blur-sm'>
<DialogHeader>
<DialogTitle className='font-semibold text-black text-xl tracking-tight'>
Reset Password
</DialogTitle>
<DialogDescription className='text-muted-foreground text-sm'>
<Modal open={forgotPasswordOpen} onOpenChange={setForgotPasswordOpen}>
<ModalContent className='dark' size='sm'>
<ModalHeader>Reset Password</ModalHeader>
<ModalBody>
<ModalDescription className='mb-4 text-[var(--text-muted)] text-sm'>
Enter your email address and we'll send you a link to reset your password if your
account exists.
</DialogDescription>
</DialogHeader>
<div className='space-y-4'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
</ModalDescription>
<div className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='reset-email'>Email</Label>
</div>
<Input
id='reset-email'
value={forgotPasswordEmail}
onChange={(e) => setForgotPasswordEmail(e.target.value)}
placeholder='Enter your email'
required
type='email'
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
resetStatus.type === 'error' &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
<Input
id='reset-email'
value={forgotPasswordEmail}
onChange={(e) => setForgotPasswordEmail(e.target.value)}
placeholder='Enter your email'
required
type='email'
className={cn(
resetStatus.type === 'error' && 'border-red-500 focus:border-red-500'
)}
/>
{resetStatus.type === 'error' && (
<div className='mt-1 text-red-400 text-xs'>
<p>{resetStatus.message}</p>
</div>
)}
/>
{resetStatus.type === 'error' && (
<div className='mt-1 space-y-1 text-red-400 text-xs'>
</div>
{resetStatus.type === 'success' && (
<div className='mt-1 text-[#4CAF50] text-xs'>
<p>{resetStatus.message}</p>
</div>
)}
<BrandedButton
type='button'
onClick={handleForgotPassword}
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
</div>
{resetStatus.type === 'success' && (
<div className='mt-1 space-y-1 text-[#4CAF50] text-xs'>
<p>{resetStatus.message}</p>
</div>
)}
<BrandedButton
type='button'
onClick={handleForgotPassword}
disabled={isSubmittingReset}
loading={isSubmittingReset}
loadingText='Sending'
>
Send Reset Link
</BrandedButton>
</div>
</DialogContent>
</Dialog>
</ModalBody>
</ModalContent>
</Modal>
</>
)
}

View File

@@ -6,8 +6,6 @@ import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/emcn'
import { signOut, useSession } from '@/lib/auth/auth-client'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
const SCOPE_DESCRIPTIONS: Record<string, string> = {
@@ -129,12 +127,10 @@ export default function OAuthConsentPage() {
return (
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-[500] text-[#ECECEC] text-[32px] tracking-tight'}>
Authorize Application
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Loading application details...
</p>
<p className={'font-[380] text-[#999] text-[16px]'}>Loading application details...</p>
</div>
</div>
)
@@ -144,14 +140,12 @@ export default function OAuthConsentPage() {
return (
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-[500] text-[#ECECEC] text-[32px] tracking-tight'}>
Authorization Error
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{error}
</p>
<p className={'font-[380] text-[#999] text-[16px]'}>{error}</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
<div className='mt-8 w-full max-w-[410px] space-y-3'>
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
</div>
</div>
@@ -172,11 +166,11 @@ export default function OAuthConsentPage() {
className='rounded-[10px]'
/>
) : (
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-muted font-medium text-[18px] text-muted-foreground'>
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] bg-[#2A2A2A] font-medium text-[#999] text-[18px]'>
{(clientName ?? '?').charAt(0).toUpperCase()}
</div>
)}
<ArrowLeftRight className='h-5 w-5 text-muted-foreground' />
<ArrowLeftRight className='h-5 w-5 text-[#999]' />
<Image
src='/new/logo/colorized-bg.svg'
alt='Sim'
@@ -187,19 +181,17 @@ export default function OAuthConsentPage() {
</div>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-[500] text-[#ECECEC] text-[32px] tracking-tight'}>
Authorize Application
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<span className='font-medium text-foreground'>{clientName}</span> is requesting access to
<p className={'font-[380] text-[#999] text-[16px]'}>
<span className='font-medium text-[#ECECEC]'>{clientName}</span> is requesting access to
your account
</p>
</div>
{session?.user && (
<div
className={`${inter.className} mt-5 flex items-center gap-3 rounded-lg border px-4 py-3`}
>
<div className='mt-5 flex items-center gap-3 rounded-lg border border-[#2A2A2A] px-4 py-3'>
{session.user.image ? (
<Image
src={session.user.image}
@@ -210,7 +202,7 @@ export default function OAuthConsentPage() {
unoptimized
/>
) : (
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-muted font-medium text-[13px] text-muted-foreground'>
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[#2A2A2A] font-medium text-[#999] text-[13px]'>
{(session.user.name ?? session.user.email ?? '?').charAt(0).toUpperCase()}
</div>
)}
@@ -218,12 +210,12 @@ export default function OAuthConsentPage() {
{session.user.name && (
<p className='truncate font-medium text-[14px]'>{session.user.name}</p>
)}
<p className='truncate text-[13px] text-muted-foreground'>{session.user.email}</p>
<p className='truncate text-[#999] text-[13px]'>{session.user.email}</p>
</div>
<button
type='button'
onClick={handleSwitchAccount}
className='ml-auto text-[13px] text-muted-foreground underline-offset-2 transition-colors hover:text-foreground hover:underline'
className='ml-auto text-[#999] text-[13px] underline-offset-2 transition-colors hover:text-[#ECECEC] hover:underline'
>
Switch
</button>
@@ -231,15 +223,12 @@ export default function OAuthConsentPage() {
)}
{scopes.length > 0 && (
<div className={`${inter.className} mt-5 w-full max-w-[410px]`}>
<div className='mt-5 w-full max-w-[410px]'>
<div className='rounded-lg border p-4'>
<p className='mb-3 font-medium text-[14px]'>This will allow the application to:</p>
<ul className='space-y-2'>
{scopes.map((s) => (
<li
key={s}
className='flex items-start gap-2 font-normal text-[13px] text-muted-foreground'
>
<li key={s} className='flex items-start gap-2 font-normal text-[#999] text-[13px]'>
<span className='mt-0.5 text-green-500'>&#10003;</span>
<span>{SCOPE_DESCRIPTIONS[s] ?? s}</span>
</li>
@@ -249,7 +238,7 @@ export default function OAuthConsentPage() {
</div>
)}
<div className={`${inter.className} mt-6 flex w-full max-w-[410px] gap-3`}>
<div className='mt-6 flex w-full max-w-[410px] gap-3'>
<Button
variant='outline'
size='md'

View File

@@ -4,8 +4,6 @@ import { Suspense, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { SetNewPasswordForm } from '@/app/(auth)/reset-password/reset-password-form'
const logger = createLogger('ResetPasswordPage')
@@ -76,15 +74,13 @@ function ResetPasswordContent() {
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>
Reset your password
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Enter a new password for your account
</p>
<p className='font-[380] text-[#999] text-[16px]'>Enter a new password for your account</p>
</div>
<div className={`${inter.className} mt-8`}>
<div className='mt-8'>
<SetNewPasswordForm
token={token}
onSubmit={handleResetPassword}
@@ -94,10 +90,10 @@ function ResetPasswordContent() {
/>
</div>
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<div className='pt-6 text-center font-light text-[14px]'>
<Link
href='/login'
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
>
Back to login
</Link>

View File

@@ -2,10 +2,8 @@
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Input, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
interface RequestResetFormProps {
@@ -33,7 +31,7 @@ export function RequestResetForm({
}
return (
<form onSubmit={handleSubmit} className={cn(`${inter.className} space-y-8`, className)}>
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -47,9 +45,8 @@ export function RequestResetForm({
type='email'
disabled={isSubmitting}
required
className='rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100'
/>
<p className='text-muted-foreground text-sm'>
<p className='text-[#999] text-sm'>
We'll send a password reset link to this email address.
</p>
</div>
@@ -142,7 +139,7 @@ export function SetNewPasswordForm({
}
return (
<form onSubmit={handleSubmit} className={cn(`${inter.className} space-y-8`, className)}>
<form onSubmit={handleSubmit} className={cn('space-y-8', className)}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -160,16 +157,12 @@ export function SetNewPasswordForm({
onChange={(e) => setPassword(e.target.value)}
required
placeholder='Enter new password'
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
validationMessage &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
className={cn('pr-10', validationMessage && 'border-red-500 focus:border-red-500')}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -192,16 +185,12 @@ export function SetNewPasswordForm({
onChange={(e) => setConfirmPassword(e.target.value)}
required
placeholder='Confirm new password'
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
validationMessage &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
)}
className={cn('pr-10', validationMessage && 'border-red-500 focus:border-red-500')}
/>
<button
type='button'
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}

View File

@@ -5,14 +5,11 @@ import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Input, Label } from '@/components/emcn'
import { client, useSession } from '@/lib/auth/auth-client'
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SocialLoginButtons } from '@/app/(auth)/components/social-login-buttons'
import { SSOLoginButton } from '@/app/(auth)/components/sso-login-button'
@@ -344,12 +341,8 @@ function SignupFormContent({
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Create an account
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Create an account or log in
</p>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>Create an account</h1>
<p className='font-[380] text-[#999] text-[16px]'>Create an account or log in</p>
</div>
{/* SSO Login Button (primary top-only when it is the only method) */}
@@ -360,7 +353,7 @@ function SignupFormContent({
const hasOnlySSO = ssoEnabled && !emailEnabled && !hasSocial
return hasOnlySSO
})() && (
<div className={`${inter.className} mt-8`}>
<div className='mt-8'>
<SSOLoginButton
callbackURL={redirectUrl || '/workspace'}
variant='primary'
@@ -371,7 +364,7 @@ function SignupFormContent({
{/* Email/Password Form - show unless explicitly disabled */}
{!isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) && (
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
<form onSubmit={onSubmit} className='mt-8 space-y-8'>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -388,10 +381,9 @@ function SignupFormContent({
value={name}
onChange={handleNameChange}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showNameValidationError &&
nameErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
/>
{showNameValidationError && nameErrors.length > 0 && (
@@ -416,9 +408,8 @@ function SignupFormContent({
value={email}
onChange={handleEmailChange}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
(emailError || (showEmailValidationError && emailErrors.length > 0)) &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
/>
{showEmailValidationError && emailErrors.length > 0 && (
@@ -450,16 +441,16 @@ function SignupFormContent({
value={password}
onChange={handlePasswordChange}
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
'pr-10',
showValidationError &&
passwordErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] transition hover:text-[#ECECEC]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
@@ -496,12 +487,12 @@ function SignupFormContent({
const showDivider = (emailEnabled || hasOnlySSO) && showBottomSection
return showDivider
})() && (
<div className={`${inter.className} relative my-6 font-light`}>
<div className='relative my-6 font-light'>
<div className='absolute inset-0 flex items-center'>
<div className='auth-divider w-full border-t' />
<div className='w-full border-[#2A2A2A] border-t' />
</div>
<div className='relative flex justify-center text-sm'>
<span className='bg-white px-4 font-[340] text-muted-foreground'>Or continue with</span>
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or continue with</span>
</div>
</div>
)}
@@ -516,7 +507,6 @@ function SignupFormContent({
})() && (
<div
className={cn(
inter.className,
isFalsy(getEnv('NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED')) ? 'mt-8' : undefined
)}
>
@@ -537,25 +527,23 @@ function SignupFormContent({
</div>
)}
<div className={`${inter.className} pt-6 text-center font-light text-[14px]`}>
<div className='pt-6 text-center font-light text-[14px]'>
<span className='font-normal'>Already have an account? </span>
<Link
href={isInviteFlow ? `/login?invite_flow=true&callbackUrl=${redirectUrl}` : '/login'}
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
>
Sign in
</Link>
</div>
<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
>
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed sm:px-8 md:px-[44px]'>
By creating an account, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Terms of Service
</Link>{' '}
@@ -564,7 +552,7 @@ function SignupFormContent({
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Privacy Policy
</Link>

View File

@@ -2,13 +2,10 @@
import { Suspense, useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { useVerification } from '@/app/(auth)/verify/use-verification'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
interface VerifyContentProps {
hasEmailService: boolean
@@ -59,15 +56,13 @@ function VerificationForm({
setCountdown(30)
}
const buttonClass = useBrandedButtonClass()
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>
{isVerified ? 'Email Verified!' : 'Verify Your Email'}
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className='font-[380] text-[#999] text-[16px]'>
{isVerified
? 'Your email has been verified. Redirecting to dashboard...'
: !isEmailVerificationEnabled
@@ -81,9 +76,9 @@ function VerificationForm({
</div>
{!isVerified && isEmailVerificationEnabled && (
<div className={`${inter.className} mt-8 space-y-8`}>
<div className='mt-8 space-y-8'>
<div className='space-y-6'>
<p className='text-center text-muted-foreground text-sm'>
<p className='text-center text-[#999] text-sm'>
Enter the 6-digit code to verify your account.
{hasEmailService ? " If you don't see it in your inbox, check your spam folder." : ''}
</p>
@@ -96,61 +91,13 @@ function VerificationForm({
disabled={isLoading}
className={cn('gap-2', isInvalidOtp && 'otp-error')}
>
<InputOTPGroup className='[&>div]:!rounded-[10px] gap-2'>
<InputOTPSlot
index={0}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
/>
<InputOTPSlot
index={1}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
/>
<InputOTPSlot
index={2}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
/>
<InputOTPSlot
index={3}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
/>
<InputOTPSlot
index={4}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
/>
<InputOTPSlot
index={5}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
isInvalidOtp && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
/>
<InputOTPGroup>
<InputOTPSlot index={0} className={cn(isInvalidOtp && 'border-red-500')} />
<InputOTPSlot index={1} className={cn(isInvalidOtp && 'border-red-500')} />
<InputOTPSlot index={2} className={cn(isInvalidOtp && 'border-red-500')} />
<InputOTPSlot index={3} className={cn(isInvalidOtp && 'border-red-500')} />
<InputOTPSlot index={4} className={cn(isInvalidOtp && 'border-red-500')} />
<InputOTPSlot index={5} className={cn(isInvalidOtp && 'border-red-500')} />
</InputOTPGroup>
</InputOTP>
</div>
@@ -163,25 +110,27 @@ function VerificationForm({
)}
</div>
<Button
<BrandedButton
onClick={verifyCode}
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
disabled={!isOtpComplete || isLoading}
loading={isLoading}
loadingText='Verifying'
showArrow={false}
>
{isLoading ? 'Verifying...' : 'Verify Email'}
</Button>
Verify Email
</BrandedButton>
{hasEmailService && (
<div className='text-center'>
<p className='text-muted-foreground text-sm'>
<p className='text-[#999] text-sm'>
Didn't receive a code?{' '}
{countdown > 0 ? (
<span>
Resend in <span className='font-medium text-foreground'>{countdown}s</span>
Resend in <span className='font-medium text-[#ECECEC]'>{countdown}s</span>
</span>
) : (
<button
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
onClick={handleResend}
disabled={isLoading || isResendDisabled}
>
@@ -202,7 +151,7 @@ function VerificationForm({
}
router.push('/signup')
}}
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
>
Back to signup
</button>
@@ -217,8 +166,8 @@ function VerificationFormFallback() {
return (
<div className='text-center'>
<div className='animate-pulse'>
<div className='mx-auto mb-4 h-8 w-48 rounded bg-gray-200' />
<div className='mx-auto h-4 w-64 rounded bg-gray-200' />
<div className='mx-auto mb-4 h-8 w-48 rounded bg-[#2A2A2A]' />
<div className='mx-auto h-4 w-64 rounded bg-[#2A2A2A]' />
</div>
</div>
)

View File

@@ -2,6 +2,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { ChevronDown } from '@/components/emcn'
import { GitHubStars } from '@/app/(home)/components/navbar/components/github-stars'
import { getBrandConfig } from '@/ee/whitelabeling'
interface NavLink {
label: string
@@ -23,7 +24,13 @@ const LOGO_CELL = 'flex items-center px-[20px]'
/** Links: even spacing between items. */
const LINK_CELL = 'flex items-center px-[14px]'
export default function Navbar() {
interface NavbarProps {
logoOnly?: boolean
}
export default function Navbar({ logoOnly = false }: NavbarProps) {
const brand = getBrandConfig()
return (
<nav
aria-label='Primary navigation'
@@ -32,66 +39,82 @@ export default function Navbar() {
itemType='https://schema.org/SiteNavigationElement'
>
{/* Logo */}
<Link href='/' className={LOGO_CELL} aria-label='Sim home' itemProp='url'>
<Link href='/' className={LOGO_CELL} aria-label={`${brand.name} home`} itemProp='url'>
<span itemProp='name' className='sr-only'>
Sim
{brand.name}
</span>
<Image
src='/logo/sim-landing.svg'
alt='Sim'
width={71}
height={22}
className='h-[22px] w-auto'
priority
/>
{brand.logoUrl ? (
<Image
src={brand.logoUrl}
alt={`${brand.name} Logo`}
width={71}
height={22}
className='h-[22px] w-auto object-contain'
priority
unoptimized
/>
) : (
<Image
src='/logo/sim-landing.svg'
alt='Sim'
width={71}
height={22}
className='h-[22px] w-auto'
priority
/>
)}
</Link>
{/* Links */}
<ul className='mt-[0.75px] flex'>
{NAV_LINKS.map(({ label, href, external, icon }) => (
<li key={label} className='flex'>
{external ? (
<a href={href} target='_blank' rel='noopener noreferrer' className={LINK_CELL}>
{label}
</a>
) : (
<Link
href={href}
className={icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL}
aria-label={label}
>
{label}
{icon === 'chevron' && (
<ChevronDown className='mt-[1.75px] h-[10px] w-[10px] flex-shrink-0 text-[#ECECEC]' />
{!logoOnly && (
<>
{/* Links */}
<ul className='mt-[0.75px] flex'>
{NAV_LINKS.map(({ label, href, external, icon }) => (
<li key={label} className='flex'>
{external ? (
<a href={href} target='_blank' rel='noopener noreferrer' className={LINK_CELL}>
{label}
</a>
) : (
<Link
href={href}
className={icon ? `${LINK_CELL} gap-[8px]` : LINK_CELL}
aria-label={label}
>
{label}
{icon === 'chevron' && (
<ChevronDown className='mt-[1.75px] h-[10px] w-[10px] flex-shrink-0 text-[#ECECEC]' />
)}
</Link>
)}
</Link>
)}
</li>
))}
<li className='flex'>
<GitHubStars />
</li>
</ul>
</li>
))}
<li className='flex'>
<GitHubStars />
</li>
</ul>
<div className='flex-1' />
<div className='flex-1' />
{/* CTAs */}
<div className='flex items-center gap-[8px] px-[20px]'>
<Link
href='/login'
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
aria-label='Log in'
>
Log in
</Link>
<Link
href='/signup'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
aria-label='Get started with Sim'
>
Get started
</Link>
</div>
{/* CTAs */}
<div className='flex items-center gap-[8px] px-[20px]'>
<Link
href='/login'
className='inline-flex h-[30px] items-center rounded-[5px] border border-[#3d3d3d] px-[9px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
aria-label='Log in'
>
Log in
</Link>
<Link
href='/signup'
className='inline-flex h-[30px] items-center gap-[7px] rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
aria-label='Get started with Sim'
>
Get started
</Link>
</div>
</>
)}
</nav>
)
}

View File

@@ -1,5 +1,4 @@
import Link from 'next/link'
import { inter } from '@/app/_styles/fonts/inter/inter'
import {
ComplianceBadges,
Logo,
@@ -14,7 +13,7 @@ interface FooterProps {
export default function Footer({ fullWidth = false }: FooterProps) {
return (
<footer className={`${inter.className} relative w-full overflow-hidden bg-white`}>
<footer className='relative w-full overflow-hidden bg-white'>
<div
className={
fullWidth

View File

@@ -23,7 +23,6 @@ import {
SupabaseIcon,
} from '@/components/icons'
import { LandingPromptStorage } from '@/lib/core/utils/browser-storage'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import {
CARD_WIDTH,
IconButton,
@@ -364,7 +363,7 @@ export default function Hero() {
return (
<section
id='hero'
className={`${soehne.className} flex w-full flex-col items-center justify-center pt-[36px] sm:pt-[80px]`}
className='flex w-full flex-col items-center justify-center pt-[36px] sm:pt-[80px]'
aria-labelledby='hero-heading'
>
<h1

View File

@@ -1,5 +1,4 @@
import * as Icons from '@/components/icons'
import { inter } from '@/app/_styles/fonts/inter/inter'
const modelProviderIcons = [
{ icon: Icons.OpenAIIcon, label: 'OpenAI' },
@@ -122,7 +121,7 @@ export default function Integrations() {
return (
<section
id='integrations'
className={`${inter.className} flex flex-col pt-[40px] pb-[27px] sm:pt-[24px]`}
className='flex flex-col pt-[40px] pb-[27px] sm:pt-[24px]'
aria-labelledby='integrations-heading'
>
<h2

View File

@@ -17,7 +17,6 @@ import {
} from 'lucide-react'
import { useRouter } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { ENTERPRISE_PLAN_FEATURES } from '@/app/workspace/[workspaceId]/settings/components/subscription/plan-configs'
const logger = createLogger('LandingPricing')
@@ -117,7 +116,6 @@ function PricingCard({
return (
<div
className={cn(
`${inter.className}`,
'relative flex h-full flex-col justify-between bg-[#FEFEFE]',
tier.featured ? 'p-0' : 'px-0 py-0',
'sm:px-5 sm:pt-4 sm:pb-4',

View File

@@ -1,5 +1,3 @@
import { inter } from '@/app/_styles/fonts/inter/inter'
interface LandingTemplatePreviewProps {
previewImage: string
avatarImage: string
@@ -37,14 +35,8 @@ export default function LandingTemplatePreview({
{/* Title and Author Info */}
<div className='min-w-0 flex-1'>
<h4
className={`${inter.className} truncate font-medium text-foreground text-sm leading-none`}
>
{title}
</h4>
<p
className={`${inter.className} mt-1 flex items-center gap-2 text-muted-foreground text-xs`}
>
<h4 className='truncate font-medium text-foreground text-sm leading-none'>{title}</h4>
<p className='mt-1 flex items-center gap-2 text-muted-foreground text-xs'>
<span>{authorName}</span>
<span>{usageCount.toLocaleString()} copies</span>
</p>

View File

@@ -1,4 +1,3 @@
import { inter } from '@/app/_styles/fonts/inter/inter'
import LandingTemplatePreview from '@/app/(landing)/components/landing-templates/components/landing-template-preview'
const templates = [
@@ -80,7 +79,7 @@ export default function LandingTemplates() {
return (
<section
id='templates'
className={`${inter.className} flex flex-col px-4 pt-[40px] sm:px-[50px] sm:pt-[34px]`}
className='flex flex-col px-4 pt-[40px] sm:px-[50px] sm:pt-[34px]'
aria-labelledby='templates-heading'
>
<h2

View File

@@ -1,36 +1,29 @@
'use client'
import { isHosted } from '@/lib/core/config/feature-flags'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import Footer from '@/app/(landing)/components/footer/footer'
import Nav from '@/app/(landing)/components/nav/nav'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
interface LegalLayoutProps {
title: string
children: React.ReactNode
navVariant?: 'landing' | 'auth' | 'legal'
}
export default function LegalLayout({ title, children, navVariant = 'legal' }: LegalLayoutProps) {
export default function LegalLayout({ title, children }: LegalLayoutProps) {
return (
<main className={`${soehne.className} min-h-screen bg-white text-gray-900`}>
{/* Header - Nav handles all conditional logic */}
<Nav variant={navVariant} />
<main className='min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
<header>
<Navbar />
</header>
{/* Content */}
<div className='px-12 pt-[40px] pb-[40px]'>
<h1 className='mb-12 text-center font-bold text-4xl text-gray-900 md:text-5xl'>{title}</h1>
<div className='prose prose-gray mx-auto prose-h2:mt-12 prose-h3:mt-8 prose-h2:mb-6 prose-h3:mb-4 space-y-8 text-gray-700'>
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
<h1 className='mb-12 text-center font-[500] text-4xl text-[#ECECEC] md:text-5xl'>
{title}
</h1>
<div className='space-y-8 text-[#999] text-[15px] leading-[1.7] [&_h2]:mt-12 [&_h2]:mb-6 [&_h2]:text-[#ECECEC] [&_h3]:mt-8 [&_h3]:mb-4 [&_h3]:text-[#ECECEC] [&_li]:text-[#999] [&_strong]:text-[#ECECEC]'>
{children}
</div>
</div>
{/* Footer - Only for hosted instances */}
{isHosted && (
<div className='relative z-20'>
<Footer fullWidth={true} />
</div>
)}
{isHosted && <Footer />}
</main>
)
}

View File

@@ -8,7 +8,6 @@ import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { GithubIcon } from '@/components/icons'
import { isHosted } from '@/lib/core/config/feature-flags'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
import { useBrandConfig } from '@/ee/whitelabeling'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
@@ -118,7 +117,7 @@ export default function Nav({ hideAuthButtons = false, variant = 'landing' }: Na
return (
<nav
aria-label='Primary navigation'
className={`${soehne.className} flex w-full items-center justify-between px-4 ${
className={`flex w-full items-center justify-between px-4 ${
variant === 'auth' ? 'pt-[20px] sm:pt-[16.5px]' : 'pt-[12px] sm:pt-[8.5px]'
} pb-[21px] sm:px-8 md:px-[44px]`}
itemScope

View File

@@ -2,7 +2,6 @@
import { useEffect, useState } from 'react'
import Image from 'next/image'
import { inter } from '@/app/_styles/fonts/inter/inter'
interface Testimonial {
text: string
@@ -123,7 +122,7 @@ export default function Testimonials() {
return (
<section
id='testimonials'
className={`flex hidden h-[150px] items-center sm:block ${inter.variable}`}
className='flex hidden h-[150px] items-center sm:block'
aria-label='Social proof testimonials'
>
<div className='relative mx-auto h-full w-full max-w-[1289px] pl-[2px]'>
@@ -180,9 +179,7 @@ export default function Testimonials() {
</div>
{/* Tweet content below with padding */}
<p
className={`${inter.className} mt-2 line-clamp-4 font-[380] text-[#0A0A0A] text-[13px] leading-[1.3] transition-colors duration-300 group-hover:text-white`}
>
<p className='mt-2 line-clamp-4 font-[380] text-[#0A0A0A] text-[13px] leading-[1.3] transition-colors duration-300 group-hover:text-white'>
{tweet.text}
</p>
</div>

View File

@@ -582,7 +582,7 @@ export default function PrivacyPolicy() {
Please note that we may ask you to verify your identity before responding to such
requests.
</p>
<p className='mb-4 border-[var(--brand-primary-hex)] border-l-4 bg-[var(--brand-primary-hex)]/10 p-3'>
<p className='mb-4 border-[#3d3d3d] border-l-4 bg-[#2A2A2A] p-3 text-[#ECECEC]'>
You have the right to complain to a Data Protection Authority about our collection and use
of your Personal Information. For more information, please contact your local data
protection authority in the European Economic Area (EEA).
@@ -604,10 +604,7 @@ export default function PrivacyPolicy() {
sharing practices (such as analytics or advertising services) may be considered a "sale"
or "share" under CCPA/CPRA. You have the right to opt-out of such data sharing. To
exercise this right, contact us at{' '}
<Link
href='mailto:privacy@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
>
<Link href='mailto:privacy@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
privacy@sim.ai
</Link>
.
@@ -693,10 +690,7 @@ export default function PrivacyPolicy() {
Sim interacts with are not covered by this policy and should be reported directly to the
solution vendor in accordance with their disclosure policy (if any). Before beginning your
inquiry, email us at{' '}
<Link
href='mailto:security@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
>
<Link href='mailto:security@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
security@sim.ai
</Link>{' '}
if you're unsure whether a system or endpoint is in scope.
@@ -715,10 +709,7 @@ export default function PrivacyPolicy() {
<h3 className='mb-2 font-medium text-xl'>Reporting a vulnerability</h3>
<p className='mb-4'>
To report any security flaws, send an email to{' '}
<Link
href='mailto:security@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
>
<Link href='mailto:security@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
security@sim.ai
</Link>
. The next business day, we'll acknowledge receipt of your vulnerability report and keep
@@ -762,7 +753,7 @@ export default function PrivacyPolicy() {
Email:{' '}
<Link
href='mailto:privacy@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
className='text-[#ECECEC] underline hover:text-white'
>
privacy@sim.ai
</Link>

View File

@@ -10,7 +10,7 @@ export function BackLink() {
return (
<Link
href='/studio'
className='group flex items-center gap-1 text-gray-600 text-sm hover:text-gray-900'
className='group flex items-center gap-1 text-[#999] text-sm hover:text-[#ECECEC]'
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>

View File

@@ -6,7 +6,6 @@ import { FAQ } from '@/lib/blog/faq'
import { getAllPostMeta, getPostBySlug, getRelatedPosts } from '@/lib/blog/registry'
import { buildArticleJsonLd, buildBreadcrumbJsonLd, buildPostMetadata } from '@/lib/blog/seo'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BackLink } from '@/app/(landing)/studio/[slug]/back-link'
import { ShareButton } from '@/app/(landing)/studio/[slug]/share-button'
@@ -36,11 +35,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
const related = await getRelatedPosts(slug, 3)
return (
<article
className={`${soehne.className} w-full`}
itemScope
itemType='https://schema.org/BlogPosting'
>
<article className='w-full' itemScope itemType='https://schema.org/BlogPosting'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
@@ -71,7 +66,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
</div>
<div className='flex flex-1 flex-col justify-between'>
<h1
className='font-medium text-[36px] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
className='font-[500] text-[#ECECEC] text-[36px] leading-tight tracking-tight sm:text-[48px] md:text-[56px] lg:text-[64px]'
itemProp='headline'
>
{post.title}
@@ -90,7 +85,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
href={a?.url || '#'}
target='_blank'
rel='noopener noreferrer author'
className='text-[14px] text-gray-600 leading-[1.5] hover:text-gray-900 sm:text-[16px]'
className='text-[#999] text-[14px] leading-[1.5] hover:text-[#ECECEC] sm:text-[16px]'
itemProp='author'
itemScope
itemType='https://schema.org/Person'
@@ -104,11 +99,11 @@ export default async function Page({ params }: { params: Promise<{ slug: string
</div>
</div>
</div>
<hr className='mt-8 border-gray-200 border-t sm:mt-12' />
<hr className='mt-8 border-[#2A2A2A] border-t sm:mt-12' />
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
<div className='flex flex-shrink-0 items-center gap-4'>
<time
className='block text-[14px] text-gray-600 leading-[1.5] sm:text-[16px]'
className='block text-[#999] text-[14px] leading-[1.5] sm:text-[16px]'
dateTime={post.date}
itemProp='datePublished'
>
@@ -121,7 +116,7 @@ export default async function Page({ params }: { params: Promise<{ slug: string
<meta itemProp='dateModified' content={post.updated ?? post.date} />
</div>
<div className='flex-1'>
<p className='m-0 block translate-y-[-4px] font-[400] text-[18px] leading-[1.5] sm:text-[20px] md:text-[26px]'>
<p className='m-0 block translate-y-[-4px] font-[400] text-[#999] text-[18px] leading-[1.5] sm:text-[20px] md:text-[26px]'>
{post.description}
</p>
</div>
@@ -129,18 +124,18 @@ export default async function Page({ params }: { params: Promise<{ slug: string
</header>
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12' itemProp='articleBody'>
<div className='prose prose-lg max-w-none'>
<div className='prose prose-lg prose-invert max-w-none prose-blockquote:border-[#3d3d3d] prose-hr:border-[#2A2A2A] prose-a:text-[#ECECEC] prose-blockquote:text-[#999] prose-code:text-[#ECECEC] prose-headings:text-[#ECECEC] prose-li:text-[#999] prose-p:text-[#999] prose-strong:text-[#ECECEC]'>
<Article />
{post.faq && post.faq.length > 0 ? <FAQ items={post.faq} /> : null}
</div>
</div>
{related.length > 0 && (
<div className='mx-auto max-w-[900px] px-6 pb-24 sm:px-8 md:px-12'>
<h2 className='mb-4 font-medium text-[24px]'>Related posts</h2>
<h2 className='mb-4 font-[500] text-[#ECECEC] text-[24px]'>Related posts</h2>
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3'>
{related.map((p) => (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
<div className='overflow-hidden rounded-lg border border-gray-200'>
<div className='overflow-hidden rounded-lg border border-[#2A2A2A]'>
<Image
src={p.ogImage}
alt={p.title}
@@ -152,14 +147,14 @@ export default async function Page({ params }: { params: Promise<{ slug: string
unoptimized
/>
<div className='p-3'>
<div className='mb-1 text-gray-600 text-xs'>
<div className='mb-1 text-[#999] text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<div className='font-medium text-sm leading-tight'>{p.title}</div>
<div className='font-[500] text-[#ECECEC] text-sm leading-tight'>{p.title}</div>
</div>
</div>
</Link>

View File

@@ -2,7 +2,12 @@
import { useState } from 'react'
import { Share2 } from 'lucide-react'
import { Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/emcn'
interface ShareButtonProps {
url: string
@@ -10,56 +15,46 @@ interface ShareButtonProps {
}
export function ShareButton({ url, title }: ShareButtonProps) {
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState(false)
const handleCopyLink = async () => {
try {
await navigator.clipboard.writeText(url)
setCopied(true)
setTimeout(() => {
setCopied(false)
setOpen(false)
}, 1000)
setTimeout(() => setCopied(false), 1500)
} catch {
setOpen(false)
/* clipboard unavailable */
}
}
const handleShareTwitter = () => {
const tweetUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`
window.open(tweetUrl, '_blank', 'noopener,noreferrer')
setOpen(false)
}
const handleShareLinkedIn = () => {
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
setOpen(false)
}
return (
<Popover
open={open}
onOpenChange={setOpen}
variant='secondary'
size='sm'
colorScheme='inverted'
>
<PopoverTrigger asChild>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className='flex items-center gap-1.5 text-gray-600 text-sm hover:text-gray-900'
className='flex items-center gap-1.5 text-[#999] text-sm hover:text-[#ECECEC]'
aria-label='Share this post'
>
<Share2 className='h-4 w-4' />
<span>Share</span>
</button>
</PopoverTrigger>
<PopoverContent align='end' minWidth={140}>
<PopoverItem onClick={handleCopyLink}>{copied ? 'Copied!' : 'Copy link'}</PopoverItem>
<PopoverItem onClick={handleShareTwitter}>Share on X</PopoverItem>
<PopoverItem onClick={handleShareLinkedIn}>Share on LinkedIn</PopoverItem>
</PopoverContent>
</Popover>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onSelect={handleCopyLink}>
{copied ? 'Copied!' : 'Copy link'}
</DropdownMenuItem>
<DropdownMenuItem onSelect={handleShareTwitter}>Share on X</DropdownMenuItem>
<DropdownMenuItem onSelect={handleShareLinkedIn}>Share on LinkedIn</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -2,7 +2,6 @@ import type { Metadata } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
export const revalidate = 3600
@@ -23,8 +22,8 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
const author = posts[0]?.author
if (!author) {
return (
<main className={`${soehne.className} mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12`}>
<h1 className='font-medium text-[32px]'>Author not found</h1>
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<h1 className='font-[500] text-[#ECECEC] text-[32px]'>Author not found</h1>
</main>
)
}
@@ -37,7 +36,7 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
image: author.avatarUrl,
}
return (
<main className={`${soehne.className} mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12`}>
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(personJsonLd) }}
@@ -53,12 +52,12 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
unoptimized
/>
) : null}
<h1 className='font-medium text-[32px] leading-tight'>{author.name}</h1>
<h1 className='font-[500] text-[#ECECEC] text-[32px] leading-tight'>{author.name}</h1>
</div>
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
{posts.map((p) => (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group'>
<div className='overflow-hidden rounded-lg border border-gray-200'>
<div className='overflow-hidden rounded-lg border border-[#2A2A2A]'>
<Image
src={p.ogImage}
alt={p.title}
@@ -68,14 +67,14 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
unoptimized
/>
<div className='p-3'>
<div className='mb-1 text-gray-600 text-xs'>
<div className='mb-1 text-[#999] text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<div className='font-medium text-sm leading-tight'>{p.title}</div>
<div className='font-[500] text-[#ECECEC] text-sm leading-tight'>{p.title}</div>
</div>
</div>
</Link>

View File

@@ -1,4 +1,5 @@
import { Footer, Nav } from '@/app/(landing)/components'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function StudioLayout({ children }: { children: React.ReactNode }) {
const orgJsonLd = {
@@ -23,7 +24,7 @@ export default function StudioLayout({ children }: { children: React.ReactNode }
}
return (
<div className='flex min-h-screen flex-col'>
<div className='flex min-h-screen flex-col bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(orgJsonLd) }}
@@ -32,9 +33,11 @@ export default function StudioLayout({ children }: { children: React.ReactNode }
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }}
/>
<Nav hideAuthButtons={false} variant='landing' />
<header>
<Navbar />
</header>
<main className='relative flex-1'>{children}</main>
<Footer fullWidth={true} />
<Footer />
</div>
)
}

View File

@@ -1,7 +1,6 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { getAllPostMeta } from '@/lib/blog/registry'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { PostGrid } from '@/app/(landing)/studio/post-grid'
export const metadata: Metadata = {
@@ -46,13 +45,15 @@ export default async function StudioIndex({
}
return (
<main className={`${soehne.className} mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12`}>
<main className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
<script
type='application/ld+json'
dangerouslySetInnerHTML={{ __html: JSON.stringify(studioJsonLd) }}
/>
<h1 className='mb-3 font-medium text-[40px] leading-tight sm:text-[56px]'>Sim Studio</h1>
<p className='mb-10 text-[18px] text-gray-700'>
<h1 className='mb-3 font-[500] text-[#ECECEC] text-[40px] leading-tight sm:text-[56px]'>
Sim Studio
</h1>
<p className='mb-10 text-[#999] text-[18px]'>
Announcements, insights, and guides for building AI agent workflows.
</p>
@@ -74,18 +75,18 @@ export default async function StudioIndex({
{pageNum > 1 && (
<Link
href={`/studio?page=${pageNum - 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
className='rounded border px-3 py-1 text-sm'
className='rounded-[5px] border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
>
Previous
</Link>
)}
<span className='text-gray-600 text-sm'>
<span className='text-[#999] text-sm'>
Page {pageNum} of {totalPages}
</span>
{pageNum < totalPages && (
<Link
href={`/studio?page=${pageNum + 1}${tag ? `&tag=${encodeURIComponent(tag)}` : ''}`}
className='rounded border px-3 py-1 text-sm'
className='rounded-[5px] border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
>
Next
</Link>

View File

@@ -27,7 +27,7 @@ export function PostGrid({ posts }: { posts: Post[] }) {
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
{posts.map((p, index) => (
<Link key={p.slug} href={`/studio/${p.slug}`} className='group flex flex-col'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-gray-200 transition-colors duration-300 hover:border-gray-300'>
<div className='flex h-full flex-col overflow-hidden rounded-xl border border-[#2A2A2A] transition-colors duration-300 hover:border-[#3d3d3d]'>
{/* Image container with fixed aspect ratio to prevent layout shift */}
<div className='relative aspect-video w-full overflow-hidden'>
<Image
@@ -42,29 +42,29 @@ export function PostGrid({ posts }: { posts: Post[] }) {
/>
</div>
<div className='flex flex-1 flex-col p-4'>
<div className='mb-2 text-gray-600 text-xs'>
<div className='mb-2 text-[#999] text-xs'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</div>
<h3 className='shine-text mb-1 font-medium text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-gray-700 text-sm'>{p.description}</p>
<h3 className='mb-1 font-[500] text-[#ECECEC] text-lg leading-tight'>{p.title}</h3>
<p className='mb-3 line-clamp-3 flex-1 text-[#999] text-sm'>{p.description}</p>
<div className='flex items-center gap-2'>
<div className='-space-x-1.5 flex'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 3)
.map((author, idx) => (
<Avatar key={idx} className='size-4 border border-white'>
<Avatar key={idx} className='size-4 border border-[#1C1C1C]'>
<AvatarImage src={author?.avatarUrl} alt={author?.name} />
<AvatarFallback className='border border-white bg-gray-100 text-[10px] text-gray-600'>
<AvatarFallback className='border border-[#1C1C1C] bg-[#2A2A2A] text-[#999] text-[10px]'>
{author?.name.slice(0, 2)}
</AvatarFallback>
</Avatar>
))}
</div>
<span className='text-gray-600 text-xs'>
<span className='text-[#999] text-xs'>
{(p.authors && p.authors.length > 0 ? p.authors : [p.author])
.slice(0, 2)
.map((a) => a?.name)

View File

@@ -10,16 +10,19 @@ export default async function TagsIndex() {
const tags = await getAllTags()
return (
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
<h1 className='mb-6 font-medium text-[32px] leading-tight'>Browse by tag</h1>
<h1 className='mb-6 font-[500] text-[#ECECEC] text-[32px] leading-tight'>Browse by tag</h1>
<div className='flex flex-wrap gap-3'>
<Link href='/studio' className='rounded-full border border-gray-300 px-3 py-1 text-sm'>
<Link
href='/studio'
className='rounded-full border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
>
All
</Link>
{tags.map((t) => (
<Link
key={t.tag}
href={`/studio?tag=${encodeURIComponent(t.tag)}`}
className='rounded-full border border-gray-300 px-3 py-1 text-sm'
className='rounded-full border border-[#3d3d3d] px-3 py-1 text-[#ECECEC] text-sm transition-colors hover:bg-[#2A2A2A]'
>
{t.tag} ({t.count})
</Link>

View File

@@ -289,7 +289,7 @@ export default function TermsOfService() {
Agreement. The arbitration will be conducted by JAMS, an established alternative dispute
resolution provider.
</p>
<p className='mb-4 border-[var(--brand-primary-hex)] border-l-4 bg-[var(--brand-primary-hex)]/10 p-3'>
<p className='mb-4 border-[#3d3d3d] border-l-4 bg-[#2A2A2A] p-3 text-[#ECECEC]'>
YOU AND COMPANY AGREE THAT EACH OF US MAY BRING CLAIMS AGAINST THE OTHER ONLY ON AN
INDIVIDUAL BASIS AND NOT ON A CLASS, REPRESENTATIVE, OR COLLECTIVE BASIS. ONLY INDIVIDUAL
RELIEF IS AVAILABLE, AND DISPUTES OF MORE THAN ONE CUSTOMER OR USER CANNOT BE ARBITRATED
@@ -298,10 +298,7 @@ export default function TermsOfService() {
<p className='mb-4'>
You have the right to opt out of the provisions of this Arbitration Agreement by sending a
timely written notice of your decision to opt out to:{' '}
<Link
href='mailto:legal@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
>
<Link href='mailto:legal@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
legal@sim.ai{' '}
</Link>
within 30 days after first becoming subject to this Arbitration Agreement.
@@ -350,7 +347,7 @@ export default function TermsOfService() {
Our Copyright Agent can be reached at:{' '}
<Link
href='mailto:copyright@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
className='text-[#ECECEC] underline hover:text-white'
>
copyright@sim.ai
</Link>
@@ -361,10 +358,7 @@ export default function TermsOfService() {
<h2 className='mb-4 font-semibold text-2xl'>18. Contact Us</h2>
<p>
If you have any questions about these Terms, please contact us at:{' '}
<Link
href='mailto:legal@sim.ai'
className='text-[var(--brand-primary-hex)] underline hover:text-[var(--brand-primary-hover-hex)]'
>
<Link href='mailto:legal@sim.ai' className='text-[#ECECEC] underline hover:text-white'>
legal@sim.ai
</Link>
</p>

View File

@@ -590,42 +590,6 @@ input[type="search"]::-ms-clear {
background-color: hsl(var(--input-background));
}
.auth-card {
background-color: var(--white) !important;
border-color: var(--border-muted) !important;
}
.dark .auth-card {
background-color: var(--surface-1) !important;
border-color: var(--border-muted) !important;
}
.auth-text-primary {
color: var(--text-inverse) !important;
}
.auth-text-secondary {
color: var(--text-secondary) !important;
}
.auth-text-muted {
color: var(--text-muted) !important;
}
.auth-divider {
border-color: var(--border-muted) !important;
}
.auth-card-shadow {
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.05),
0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
}
.auth-link {
color: var(--text-muted) !important;
}
.transition-ring {
transition-property: box-shadow, transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);

View File

@@ -1,27 +0,0 @@
'use client'
import Link from 'next/link'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
interface BrandedLinkProps {
href: string
children: React.ReactNode
className?: string
target?: string
rel?: string
}
export function BrandedLink({ href, children, className = '', target, rel }: BrandedLinkProps) {
const buttonClass = useBrandedButtonClass()
return (
<Link
href={href}
target={target}
rel={rel}
className={`${buttonClass} group inline-flex items-center justify-center gap-2 rounded-[10px] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white transition-all ${className}`}
>
{children}
</Link>
)
}

View File

@@ -1,8 +1,5 @@
import { BookOpen, Github, Rss } from 'lucide-react'
import Link from 'next/link'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedLink } from '@/app/changelog/components/branded-link'
import ChangelogList from '@/app/changelog/components/timeline-list'
export interface ChangelogEntry {
@@ -47,44 +44,38 @@ export default async function ChangelogContent() {
}
return (
<div className='min-h-screen bg-background'>
<div className='min-h-screen'>
<div className='relative grid md:grid-cols-2'>
{/* Left intro panel */}
<div className='relative top-0 overflow-hidden border-border border-b px-6 py-16 sm:px-10 md:sticky md:h-dvh md:border-r md:border-b-0 md:px-12 md:py-24'>
<div className='absolute inset-0 bg-grid-pattern opacity-[0.03] dark:opacity-[0.06]' />
<div className='absolute inset-0 bg-gradient-to-tr from-background via-transparent to-background/60' />
<div className='relative top-0 overflow-hidden border-[#2A2A2A] border-b px-6 py-16 sm:px-10 md:sticky md:h-dvh md:border-r md:border-b-0 md:px-12 md:py-24'>
<div className='relative mx-auto h-full max-w-xl md:flex md:flex-col md:justify-center'>
<h1
className={`${soehne.className} mt-6 font-semibold text-4xl tracking-tight sm:text-5xl`}
>
Changelog
</h1>
<p className={`${inter.className} mt-4 text-muted-foreground text-sm`}>
<h1 className='mt-6 font-[500] text-4xl tracking-tight sm:text-5xl'>Changelog</h1>
<p className='mt-4 text-[#999] text-sm'>
Stay up-to-date with the latest features, improvements, and bug fixes in Sim. All
changes are documented here with detailed release notes.
</p>
<hr className='mt-6 border-border' />
<hr className='mt-6 border-[#2A2A2A]' />
<div className='mt-6 flex flex-wrap items-center gap-3 text-sm'>
<BrandedLink
<Link
href='https://github.com/simstudioai/sim/releases'
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center gap-2 rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] py-[5px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
<Github className='h-4 w-4' />
View on GitHub
</BrandedLink>
</Link>
<Link
href='https://docs.sim.ai'
className='inline-flex items-center gap-2 rounded-[10px] border border-border py-[6px] pr-[10px] pl-[12px] text-[15px] transition-all hover:bg-muted'
className='inline-flex items-center gap-2 rounded-[5px] border border-[#3d3d3d] px-[9px] py-[5px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
>
<BookOpen className='h-4 w-4' />
Documentation
</Link>
<Link
href='/changelog.xml'
className='inline-flex items-center gap-2 rounded-[10px] border border-border py-[6px] pr-[10px] pl-[12px] text-[15px] transition-all hover:bg-muted'
className='inline-flex items-center gap-2 rounded-[5px] border border-[#3d3d3d] px-[9px] py-[5px] text-[#ECECEC] text-[13.5px] transition-colors hover:bg-[#2A2A2A]'
>
<Rss className='h-4 w-4' />
RSS Feed

View File

@@ -3,8 +3,6 @@
import React from 'react'
import ReactMarkdown from 'react-markdown'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/emcn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import type { ChangelogEntry } from '@/app/changelog/components/changelog-content'
type Props = { initialEntries: ChangelogEntry[] }
@@ -100,7 +98,7 @@ export default function ChangelogList({ initialEntries }: Props) {
<div key={entry.tag}>
<div className='flex items-center justify-between gap-4'>
<div className='flex items-center gap-2'>
<div className={`${soehne.className} font-semibold text-[18px] tracking-tight`}>
<div className='font-[500] text-[#ECECEC] text-[18px] tracking-tight'>
{entry.tag}
</div>
{entry.contributors && entry.contributors.length > 0 && (
@@ -115,7 +113,7 @@ export default function ChangelogList({ initialEntries }: Props) {
title={`@${contributor}`}
className='block'
>
<Avatar className='size-6 ring-2 ring-background'>
<Avatar className='size-6 ring-2 ring-[#1C1C1C]'>
<AvatarImage
src={`https://avatars.githubusercontent.com/${contributor}`}
alt={`@${contributor}`}
@@ -126,14 +124,14 @@ export default function ChangelogList({ initialEntries }: Props) {
</a>
))}
{entry.contributors.length > 5 && (
<div className='relative flex size-6 items-center justify-center rounded-full bg-muted text-[10px] text-foreground ring-2 ring-background hover:z-10'>
<div className='relative flex size-6 items-center justify-center rounded-full bg-[#2A2A2A] text-[#ECECEC] text-[10px] ring-2 ring-[#1C1C1C] hover:z-10'>
+{entry.contributors.length - 5}
</div>
)}
</div>
)}
</div>
<div className={`${inter.className} text-muted-foreground text-xs`}>
<div className='text-[#999] text-xs'>
{new Date(entry.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
@@ -142,15 +140,13 @@ export default function ChangelogList({ initialEntries }: Props) {
</div>
</div>
<div
className={`${inter.className} prose prose-sm dark:prose-invert max-w-none prose-headings:font-semibold prose-a:text-brand-primary prose-headings:text-foreground prose-p:text-muted-foreground prose-a:no-underline hover:prose-a:underline`}
>
<div className='max-w-none'>
<ReactMarkdown
components={{
h2: ({ children, ...props }) =>
isContributorsLabel(children) ? null : (
<h3
className={`${soehne.className} mt-5 mb-2 font-medium text-[13px] text-foreground tracking-tight`}
className='mt-5 mb-2 font-[500] text-[#ECECEC] text-[13px] tracking-tight'
{...props}
>
{children}
@@ -159,7 +155,7 @@ export default function ChangelogList({ initialEntries }: Props) {
h3: ({ children, ...props }) =>
isContributorsLabel(children) ? null : (
<h4
className={`${soehne.className} mt-4 mb-1 font-medium text-[13px] text-foreground tracking-tight`}
className='mt-4 mb-1 font-[500] text-[#ECECEC] text-[13px] tracking-tight'
{...props}
>
{children}
@@ -174,28 +170,25 @@ export default function ChangelogList({ initialEntries }: Props) {
const text = String(children)
if (/^\s*contributors\s*:?\s*$/i.test(text)) return null
return (
<li className='text-[13px] text-muted-foreground leading-relaxed' {...props}>
<li className='text-[#999] text-[13px] leading-relaxed' {...props}>
{children}
</li>
)
},
p: ({ children, ...props }) =>
/^\s*contributors\s*:?\s*$/i.test(String(children)) ? null : (
<p
className='mb-3 text-[13px] text-muted-foreground leading-relaxed'
{...props}
>
<p className='mb-3 text-[#999] text-[13px] leading-relaxed' {...props}>
{children}
</p>
),
strong: ({ children, ...props }) => (
<strong className='font-medium text-foreground' {...props}>
<strong className='font-[500] text-[#ECECEC]' {...props}>
{children}
</strong>
),
code: ({ children, ...props }) => (
<code
className='rounded bg-muted px-1 py-0.5 font-mono text-foreground text-xs'
className='rounded bg-[#2A2A2A] px-1 py-0.5 font-mono text-[#ECECEC] text-xs'
{...props}
>
{children}
@@ -224,7 +217,7 @@ export default function ChangelogList({ initialEntries }: Props) {
type='button'
onClick={loadMore}
disabled={loading}
className='rounded-md border border-border px-3 py-1.5 text-[13px] hover:bg-muted disabled:opacity-60'
className='rounded-[5px] border border-[#3d3d3d] px-3 py-1.5 text-[#ECECEC] text-[13px] transition-colors hover:bg-[#2A2A2A] disabled:opacity-60'
>
{loading ? 'Loading…' : 'Show more'}
</button>

View File

@@ -1,11 +1,17 @@
import Nav from '@/app/(landing)/components/nav/nav'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import Footer from '@/app/(home)/components/footer/footer'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function ChangelogLayout({ children }: { children: React.ReactNode }) {
return (
<div className='relative min-h-screen text-foreground'>
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
<Nav />
<div
className={`${martianMono.variable} relative min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]`}
>
<header>
<Navbar />
</header>
{children}
<Footer />
</div>
)
}

View File

@@ -2,17 +2,13 @@
import { type KeyboardEvent, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Input } from '@/components/emcn'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
import { Label } from '@/components/ui/label'
import { Input, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import AuthBackground from '@/app/(auth)/components/auth-background'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
const logger = createLogger('EmailAuth')
@@ -185,26 +181,26 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps)
}
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-screen flex-col text-[#ECECEC]'>
<header>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>
{showOtpVerification ? 'Verify Your Email' : 'Email Verification'}
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className='font-[380] text-[#999] text-[16px]'>
{showOtpVerification
? `A verification code has been sent to ${email}`
: 'This chat requires email verification'}
</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px]`}>
<div className='mt-8 w-full max-w-[410px]'>
{!showOtpVerification ? (
<form
onSubmit={(e) => {
@@ -229,10 +225,9 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps)
onChange={handleEmailChange}
onKeyDown={handleEmailKeyDown}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showEmailValidationError &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
autoFocus
/>
@@ -274,13 +269,7 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps)
<InputOTPSlot
key={index}
index={index}
className={cn(
'!rounded-[10px] h-12 w-12 border bg-white text-center font-medium text-lg shadow-sm transition-all duration-200',
'border-gray-300 hover:border-gray-400',
'focus:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-100',
authError &&
'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
className={cn('!rounded-[10px] h-12 w-12', authError && 'border-red-500')}
/>
))}
</InputOTPGroup>

View File

@@ -3,15 +3,12 @@
import { type KeyboardEvent, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Eye, EyeOff } from 'lucide-react'
import { Input } from '@/components/emcn'
import { Label } from '@/components/ui/label'
import { Input, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import AuthBackground from '@/app/(auth)/components/auth-background'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
const logger = createLogger('PasswordAuth')
@@ -82,19 +79,19 @@ export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuth
}
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-screen flex-col text-[#ECECEC]'>
<header>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>
Password Required
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className='font-[380] text-[#999] text-[16px]'>
This chat is password-protected
</p>
</div>
@@ -104,7 +101,7 @@ export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuth
e.preventDefault()
handleAuthenticate()
}}
className={`${inter.className} mt-8 w-full max-w-[410px] space-y-6`}
className='mt-8 w-full max-w-[410px] space-y-6'
>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -124,17 +121,17 @@ export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuth
onChange={handlePasswordChange}
onKeyDown={handleKeyDown}
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
'pr-10',
showValidationError &&
passwordErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
autoFocus
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-gray-500 transition hover:text-gray-700'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] hover:text-[#ECECEC]'
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}

View File

@@ -3,7 +3,6 @@
import Image from 'next/image'
import Link from 'next/link'
import { GithubIcon } from '@/components/icons'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { useBrandConfig } from '@/ee/whitelabeling'
interface ChatHeaderProps {
@@ -41,7 +40,7 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
className='h-6 w-6 rounded-md object-cover'
/>
)}
<h2 className={`${inter.className} font-medium text-[18px] text-foreground`}>
<h2 className='font-medium text-[18px] text-foreground'>
{chatConfig?.customizations?.headerText || chatConfig?.title || 'Chat'}
</h2>
</div>
@@ -57,9 +56,7 @@ export function ChatHeader({ chatConfig, starCount }: ChatHeaderProps) {
aria-label={`GitHub repository - ${starCount} stars`}
>
<GithubIcon className='h-[16px] w-[16px]' aria-hidden='true' />
<span className={`${inter.className}`} aria-live='polite'>
{starCount}
</span>
<span aria-live='polite'>{starCount}</span>
</a>
{/* Only show Sim logo if no custom branding is set */}

View File

@@ -12,7 +12,7 @@ export function FormErrorState({ error }: FormErrorStateProps) {
const router = useRouter()
return (
<StatusPageLayout title='Form Unavailable' description={error} hideNav>
<StatusPageLayout title='Form Unavailable' description={error}>
<BrandedButton onClick={() => router.push('/workspace')}>Return to Workspace</BrandedButton>
</StatusPageLayout>
)

View File

@@ -4,7 +4,6 @@ import { useCallback, useRef, useState } from 'react'
import { Upload, X } from 'lucide-react'
import { Input, Label, Switch, Textarea } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
interface InputField {
name: string
@@ -96,9 +95,7 @@ export function FormField({
onCheckedChange={onChange}
style={value ? { backgroundColor: primaryColor } : undefined}
/>
<span className={`${inter.className} text-[14px] text-muted-foreground`}>
{value ? 'Yes' : 'No'}
</span>
<span className={'text-[14px] text-muted-foreground'}>{value ? 'Yes' : 'No'}</span>
</div>
)
@@ -159,7 +156,7 @@ export function FormField({
className='mb-2 h-6 w-6 text-muted-foreground'
style={isDragging ? { color: primaryColor } : undefined}
/>
<p className={`${inter.className} text-center text-[14px] text-muted-foreground`}>
<p className={'text-center text-[14px] text-muted-foreground'}>
<span style={{ color: primaryColor }} className='font-medium'>
Click to upload
</span>{' '}
@@ -175,12 +172,10 @@ export function FormField({
className='flex items-center justify-between rounded-[8px] border border-border bg-muted/30 px-3 py-2'
>
<div className='min-w-0 flex-1'>
<p
className={`${inter.className} truncate font-medium text-[13px] text-foreground`}
>
<p className={'truncate font-medium text-[13px] text-foreground'}>
{file.name}
</p>
<p className={`${inter.className} text-[12px] text-muted-foreground`}>
<p className={'text-[12px] text-muted-foreground'}>
{formatFileSize(file.size)}
</p>
</div>
@@ -217,7 +212,7 @@ export function FormField({
return (
<div className='space-y-2'>
<Label className={`${inter.className} font-medium text-[14px] text-foreground`}>
<Label className={'font-medium text-[14px] text-foreground'}>
{displayLabel}
{isRequired && <span className='ml-0.5 text-[var(--text-error)]'>*</span>}
</Label>

View File

@@ -2,14 +2,12 @@
import { useState } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { Input } from '@/components/emcn'
import { Input, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import AuthBackground from '@/app/(auth)/components/auth-background'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
interface PasswordAuthProps {
onSubmit: (password: string) => void
@@ -34,34 +32,26 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) {
}
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-screen flex-col text-[#ECECEC]'>
<header>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>
Password Required
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className='font-[380] text-[#999] text-[16px]'>
Enter the password to access this form.
</p>
</div>
<form
onSubmit={handleSubmit}
className={`${inter.className} mt-8 w-full max-w-[410px] space-y-6`}
>
<form onSubmit={handleSubmit} className='mt-8 w-full max-w-[410px] space-y-6'>
<div className='space-y-2'>
<label
htmlFor='form-password'
className='font-medium text-[14px] text-foreground'
>
Password
</label>
<Label htmlFor='form-password'>Password</Label>
<div className='relative'>
<Input
id='form-password'
@@ -69,16 +59,13 @@ export function PasswordAuth({ onSubmit, error }: PasswordAuthProps) {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder='Enter password'
className={cn(
'rounded-[10px] pr-10 shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
error && 'border-red-500 focus:border-red-500 focus:ring-red-100'
)}
className={cn(error && 'border-red-500 focus:border-red-500')}
autoFocus
/>
<button
type='button'
onClick={() => setShowPassword(!showPassword)}
className='-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground hover:text-foreground'
className='-translate-y-1/2 absolute top-1/2 right-3 text-[#999] hover:text-[#ECECEC]'
>
{showPassword ? <EyeOff className='h-4 w-4' /> : <Eye className='h-4 w-4' />}
</button>

View File

@@ -1,7 +1,6 @@
'use client'
import Image from 'next/image'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { useBrandConfig } from '@/ee/whitelabeling'
export function PoweredBySim() {
@@ -9,7 +8,9 @@ export function PoweredBySim() {
return (
<div
className={`${inter.className} auth-text-muted fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed`}
className={
'fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[#999] text-[13px] leading-relaxed'
}
>
<a
href='https://sim.ai'

View File

@@ -1,8 +1,6 @@
'use client'
import { CheckCircle2 } from 'lucide-react'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
interface ThankYouScreenProps {
title: string
@@ -31,16 +29,12 @@ export function ThankYouScreen({ title, message, primaryColor }: ThankYouScreenP
<CheckCircle2 className='h-10 w-10' style={{ color: thankYouColor }} />
</div>
<h2
className={`${soehne.className} mt-6 font-medium text-[32px] tracking-tight`}
className={'mt-6 font-[500] text-[32px] tracking-tight'}
style={{ color: thankYouColor }}
>
{title}
</h2>
<p
className={`${inter.className} mt-3 max-w-md font-[380] text-[16px] text-muted-foreground`}
>
{message}
</p>
<p className={'mt-3 max-w-md font-[380] text-[#999] text-[16px]'}>{message}</p>
</div>
</main>
)

View File

@@ -21,7 +21,6 @@ export default function FormError({ error, reset }: FormErrorProps) {
<StatusPageLayout
title='Something went wrong'
description='We encountered an error loading this form. Please try again.'
hideNav
>
<BrandedButton onClick={reset}>Try again</BrandedButton>
</StatusPageLayout>

View File

@@ -2,8 +2,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono'
import AuthBackground from '@/app/(auth)/components/auth-background'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
@@ -238,8 +237,8 @@ export default function Form({ identifier }: { identifier: string }) {
if (isSubmitted && thankYouData) {
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<AuthBackground className={`${martianMono.variable} dark font-[430] font-season`}>
<main className='relative flex min-h-screen flex-col text-[#ECECEC]'>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<ThankYouScreen
title={thankYouData.title}
@@ -270,29 +269,23 @@ export default function Form({ identifier }: { identifier: string }) {
)
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<AuthBackground className={`${martianMono.variable} dark font-[430] font-season`}>
<main className='relative flex min-h-screen flex-col text-[#ECECEC]'>
<div className='relative z-30 flex flex-1 justify-center px-4 pt-16 pb-24'>
<div className='w-full max-w-[410px]'>
{/* Form title */}
<div className='mb-8 text-center'>
<h1
className={`${soehne.className} font-medium text-[28px] text-foreground tracking-tight`}
>
<h1 className='font-[500] text-[#ECECEC] text-[28px] tracking-tight'>
{formConfig.title}
</h1>
{formConfig.description && (
<p
className={`${inter.className} mt-2 font-[380] text-[15px] text-muted-foreground`}
>
{formConfig.description}
</p>
<p className='mt-2 font-[380] text-[#999] text-[15px]'>{formConfig.description}</p>
)}
</div>
<form onSubmit={handleSubmit} className={`${inter.className} space-y-6`}>
<form onSubmit={handleSubmit} className='space-y-6'>
{fields.length === 0 ? (
<div className='rounded-[10px] border border-border bg-muted/50 p-6 text-center text-muted-foreground'>
<div className='rounded-[10px] border border-[#2A2A2A] bg-[#2A2A2A] p-6 text-center text-[#999]'>
This form has no fields configured.
</div>
) : (

View File

@@ -1,7 +1,6 @@
'use client'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
interface InviteLayoutProps {
children: React.ReactNode
@@ -9,15 +8,15 @@ interface InviteLayoutProps {
export default function InviteLayout({ children }: InviteLayoutProps) {
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>{children}</div>
</div>
<div className='relative min-h-screen bg-[#1C1C1C] font-[430] font-season text-[#ECECEC]'>
<header>
<Navbar logoOnly />
</header>
<main className='flex flex-1 flex-col items-center justify-center px-4 pt-[15vh]'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>{children}</div>
</div>
</main>
</AuthBackground>
</div>
)
}

View File

@@ -2,10 +2,7 @@
import { Loader2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
interface InviteStatusCardProps {
type: 'login' | 'loading' | 'error' | 'success' | 'invitation' | 'warning'
@@ -35,17 +32,12 @@ export function InviteStatusCard({
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Loading
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{description}
</p>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>Loading</h1>
<p className='font-[380] text-[#999] text-[16px]'>{description}</p>
</div>
<div className={`${inter.className} mt-8 flex w-full items-center justify-center py-8`}>
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
<div className='mt-8 flex w-full items-center justify-center py-8'>
<Loader2 className='h-8 w-8 animate-spin text-[#999]' />
</div>
<SupportFooter position='absolute' />
</>
)
}
@@ -53,33 +45,35 @@ export function InviteStatusCard({
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
{title}
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{description}
</p>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>{title}</h1>
<p className='font-[380] text-[#999] text-[16px]'>{description}</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
<div className='mt-8 w-full max-w-[410px] space-y-3'>
{isExpiredError && (
<BrandedButton onClick={() => router.push('/')}>Request New Invitation</BrandedButton>
<BrandedButton onClick={() => router.push('/')} showArrow={false}>
Request New Invitation
</BrandedButton>
)}
{actions.map((action, index) => (
<BrandedButton
key={index}
onClick={action.onClick}
disabled={action.disabled}
disabled={action.disabled || action.loading}
loading={action.loading}
loadingText={action.label}
showArrow={false}
className={
index !== 0
? 'border-[#3d3d3d] bg-transparent text-[#ECECEC] hover:border-[#3d3d3d] hover:bg-[#2A2A2A]'
: undefined
}
>
{action.label}
</BrandedButton>
))}
</div>
<SupportFooter position='absolute' />
</>
)
}

View File

@@ -1,18 +1,27 @@
'use client'
import { useRouter } from 'next/navigation'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { StatusPageLayout } from '@/app/(auth)/components/status-page-layout'
import Link from 'next/link'
import AuthBackground from '@/app/(auth)/components/auth-background'
import Navbar from '@/app/(home)/components/navbar/navbar'
export default function NotFound() {
const router = useRouter()
return (
<StatusPageLayout
title='Page Not Found'
description="The page you're looking for doesn't exist or has been moved."
>
<BrandedButton onClick={() => router.push('/')}>Return to Home</BrandedButton>
</StatusPageLayout>
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-full flex-col text-[#ECECEC]'>
<header className='shrink-0 bg-[#1C1C1C]'>
<Navbar />
</header>
<div className='relative z-30 flex flex-1 flex-col items-center justify-center px-4 pb-24'>
<h1 className='font-[500] text-[48px] tracking-tight'>Page Not Found</h1>
<p className='mt-2 text-[#999] text-[16px]'>
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<Link
href='/'
className='mt-8 inline-flex h-[30px] items-center rounded-[5px] border border-[#FFFFFF] bg-[#FFFFFF] px-[9px] text-[13.5px] text-black transition-colors hover:border-[#E0E0E0] hover:bg-[#E0E0E0]'
>
Return to Home
</Link>
</div>
</main>
</AuthBackground>
)
}

View File

@@ -24,7 +24,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
import { useBrandConfig } from '@/ee/whitelabeling'
import type { ResumeStatus } from '@/executor/types'
@@ -809,8 +809,10 @@ export default function ResumeExecutionPage({
if (!executionDetail) {
return (
<Tooltip.Provider>
<div style={{ minHeight: '100vh', background: 'var(--bg)' }}>
<Nav variant='auth' />
<div className='font-season' style={{ minHeight: '100vh', background: 'var(--bg)' }}>
<header>
<Navbar />
</header>
<div
style={{
display: 'flex',
@@ -846,8 +848,10 @@ export default function ResumeExecutionPage({
return (
<Tooltip.Provider>
<div style={{ minHeight: '100vh', background: 'var(--bg)' }}>
<Nav variant='auth' />
<div className='font-season' style={{ minHeight: '100vh', background: 'var(--bg)' }}>
<header>
<Navbar />
</header>
<div style={{ maxWidth: '1200px', margin: '0 auto', padding: '32px 24px' }}>
{/* Header */}
<div

View File

@@ -3,8 +3,6 @@
import { Suspense, useEffect, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { useSearchParams } from 'next/navigation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
import { InviteLayout } from '@/app/invite/components'
@@ -122,15 +120,13 @@ function UnsubscribeContent() {
return (
<InviteLayout>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Loading
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<h1 className={'font-medium text-[#ECECEC] text-[32px] tracking-tight'}>Loading</h1>
<p className={'font-[380] text-[#999] text-[16px]'}>
Validating your unsubscribe link...
</p>
</div>
<div className={`${inter.className} mt-8 flex w-full items-center justify-center py-8`}>
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
<div className={'mt-8 flex w-full items-center justify-center py-8'}>
<Loader2 className='h-8 w-8 animate-spin text-[#999]' />
</div>
<SupportFooter position='absolute' />
</InviteLayout>
@@ -141,15 +137,13 @@ function UnsubscribeContent() {
return (
<InviteLayout>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-medium text-[#ECECEC] text-[32px] tracking-tight'}>
Invalid Unsubscribe Link
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
{error}
</p>
<p className={'font-[380] text-[#999] text-[16px]'}>{error}</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
<BrandedButton onClick={() => window.history.back()}>Go Back</BrandedButton>
</div>
@@ -162,16 +156,16 @@ function UnsubscribeContent() {
return (
<InviteLayout>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-medium text-[#ECECEC] text-[32px] tracking-tight'}>
Important Account Emails
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className={'font-[380] text-[#999] text-[16px]'}>
Transactional emails like password resets, account confirmations, and security alerts
cannot be unsubscribed from as they contain essential information for your account.
</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
<BrandedButton onClick={() => window.close()}>Close</BrandedButton>
</div>
@@ -184,16 +178,16 @@ function UnsubscribeContent() {
return (
<InviteLayout>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-medium text-[#ECECEC] text-[32px] tracking-tight'}>
Successfully Unsubscribed
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className={'font-[380] text-[#999] text-[16px]'}>
You have been unsubscribed from our emails. You will stop receiving emails within 48
hours.
</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
<BrandedButton onClick={() => window.close()}>Close</BrandedButton>
</div>
@@ -207,18 +201,16 @@ function UnsubscribeContent() {
return (
<InviteLayout>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
<h1 className={'font-medium text-[#ECECEC] text-[32px] tracking-tight'}>
Email Preferences
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className={'font-[380] text-[#999] text-[16px]'}>
Choose which emails you'd like to stop receiving.
</p>
<p className={`${inter.className} mt-2 font-[380] text-[14px] text-muted-foreground`}>
{data?.email}
</p>
<p className={'mt-2 font-[380] text-[#999] text-[14px]'}>{data?.email}</p>
</div>
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
<div className={'mt-8 w-full max-w-[410px] space-y-3'}>
<BrandedButton
onClick={() => handleUnsubscribe('all')}
disabled={processing || isAlreadyUnsubscribedFromAll}
@@ -231,9 +223,7 @@ function UnsubscribeContent() {
</BrandedButton>
<div className='py-2 text-center'>
<span className={`${inter.className} font-[380] text-[14px] text-muted-foreground`}>
or choose specific types
</span>
<span className={'font-[380] text-[#999] text-[14px]'}>or choose specific types</span>
</div>
<BrandedButton
@@ -276,8 +266,8 @@ function UnsubscribeContent() {
</BrandedButton>
</div>
<div className={`${inter.className} mt-6 max-w-[410px] text-center`}>
<p className='font-[380] text-[13px] text-muted-foreground'>
<div className={'mt-6 max-w-[410px] text-center'}>
<p className='font-[380] text-[#999] text-[13px]'>
You'll continue receiving important account emails like password resets and security
alerts.
</p>
@@ -294,15 +284,13 @@ export default function Unsubscribe() {
fallback={
<InviteLayout>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Loading
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<h1 className={'font-medium text-[#ECECEC] text-[32px] tracking-tight'}>Loading</h1>
<p className={'font-[380] text-[#999] text-[16px]'}>
Validating your unsubscribe link...
</p>
</div>
<div className={`${inter.className} mt-8 flex w-full items-center justify-center py-8`}>
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
<div className={'mt-8 flex w-full items-center justify-center py-8'}>
<Loader2 className='h-8 w-8 animate-spin text-[#999]' />
</div>
<SupportFooter position='absolute' />
</InviteLayout>

View File

@@ -20,7 +20,6 @@ import {
Textarea,
Tooltip,
} from '@/components/emcn'
import { Alert, AlertDescription } from '@/components/ui'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { generatePassword } from '@/lib/core/security/encryption'
import { cn } from '@/lib/core/utils/cn'
@@ -323,10 +322,10 @@ export function ChatDeploy({
className='-mx-1 space-y-4 overflow-y-auto px-1'
>
{errors.general && (
<Alert variant='destructive'>
<AlertTriangle className='h-4 w-4' />
<AlertDescription>{errors.general}</AlertDescription>
</Alert>
<div className='flex items-center gap-2 rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-[13px] text-red-400'>
<AlertTriangle className='h-4 w-4 flex-shrink-0' />
<span>{errors.general}</span>
</div>
)}
<div className='space-y-[12px]'>

View File

@@ -2,14 +2,15 @@
import { useMemo, useState } from 'react'
import { Settings2 } from 'lucide-react'
import { Button, Checkbox } from '@/components/emcn/components'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
Button,
Checkbox,
Modal,
ModalBody,
ModalContent,
ModalHeader,
ModalTrigger,
} from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
@@ -100,8 +101,8 @@ export function GroupedCheckboxList({
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Modal open={open} onOpenChange={setOpen}>
<ModalTrigger asChild>
<Button
variant='ghost'
disabled={disabled}
@@ -116,82 +117,83 @@ export function GroupedCheckboxList({
</span>
<SelectedCountDisplay />
</Button>
</DialogTrigger>
<DialogContent
className='flex max-h-[80vh] max-w-2xl flex-col'
</ModalTrigger>
<ModalContent
size='lg'
className='flex max-h-[80vh] flex-col'
onWheel={(e) => e.stopPropagation()}
>
<DialogHeader>
<DialogTitle>Select PII Types to Detect</DialogTitle>
<p className='text-muted-foreground text-sm'>
<ModalHeader>Select PII Types to Detect</ModalHeader>
<ModalBody>
<p className='mb-3 text-[var(--text-muted)] text-sm'>
Choose which types of personally identifiable information to detect and block.
</p>
</DialogHeader>
{/* Header with Select All and Clear */}
<div className='flex items-center justify-between border-b pb-3'>
<div className='flex items-center gap-2'>
<Checkbox
id='select-all'
checked={allSelected}
onCheckedChange={(checked) => {
if (checked) {
handleSelectAll()
} else {
handleClear()
}
}}
disabled={disabled}
/>
<label
htmlFor='select-all'
className='cursor-pointer font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Select all entities
</label>
{/* Header with Select All and Clear */}
<div className='flex items-center justify-between border-b pb-3'>
<div className='flex items-center gap-2'>
<Checkbox
id='select-all'
checked={allSelected}
onCheckedChange={(checked) => {
if (checked) {
handleSelectAll()
} else {
handleClear()
}
}}
disabled={disabled}
/>
<label
htmlFor='select-all'
className='cursor-pointer font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Select all entities
</label>
</div>
<Button variant='ghost' onClick={handleClear} disabled={disabled || noneSelected}>
<span className='flex items-center gap-1'>
Clear{!noneSelected && <span>({selectedValues.length})</span>}
</span>
</Button>
</div>
<Button variant='ghost' onClick={handleClear} disabled={disabled || noneSelected}>
<span className='flex items-center gap-1'>
Clear{!noneSelected && <span>({selectedValues.length})</span>}
</span>
</Button>
</div>
{/* Scrollable grouped checkboxes */}
<div
className='flex-1 overflow-y-auto pr-4'
onWheel={(e) => e.stopPropagation()}
style={{ maxHeight: '60vh' }}
>
<div className='space-y-6'>
{Object.entries(groupedOptions).map(([groupName, groupOptions]) => (
<div key={groupName}>
<h3 className='mb-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider'>
{groupName}
</h3>
<div className='space-y-3'>
{groupOptions.map((option) => (
<div key={option.id} className='flex items-center gap-2'>
<Checkbox
id={`${subBlockId}-${option.id}`}
checked={selectedValues.includes(option.id)}
onCheckedChange={() => handleToggle(option.id)}
disabled={disabled}
/>
<label
htmlFor={`${subBlockId}-${option.id}`}
className='cursor-pointer text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
{option.label}
</label>
</div>
))}
{/* Scrollable grouped checkboxes */}
<div
className='flex-1 overflow-y-auto pr-4'
onWheel={(e) => e.stopPropagation()}
style={{ maxHeight: '60vh' }}
>
<div className='space-y-6'>
{Object.entries(groupedOptions).map(([groupName, groupOptions]) => (
<div key={groupName}>
<h3 className='mb-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider'>
{groupName}
</h3>
<div className='space-y-3'>
{groupOptions.map((option) => (
<div key={option.id} className='flex items-center gap-2'>
<Checkbox
id={`${subBlockId}-${option.id}`}
checked={selectedValues.includes(option.id)}
onCheckedChange={() => handleToggle(option.id)}
disabled={disabled}
/>
<label
htmlFor={`${subBlockId}-${option.id}`}
className='cursor-pointer text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
{option.label}
</label>
</div>
))}
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
</DialogContent>
</Dialog>
</ModalBody>
</ModalContent>
</Modal>
)
}

View File

@@ -24,6 +24,8 @@ import { useSuperUserStatus } from '@/hooks/queries/user-profile'
import { usePermissionConfig } from '@/hooks/use-permission-config'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
const SKELETON_SECTIONS = [3, 2, 2] as const
interface SettingsSidebarProps {
isCollapsed?: boolean
showCollapsedContent?: boolean
@@ -205,9 +207,22 @@ export function SettingsSidebar({
!isCollapsed && 'overflow-y-auto overflow-x-hidden'
)}
>
{sessionLoading || orgsLoading
? Array.from({ length: 3 }, (_, i) => (
<div key={i} className='flex flex-shrink-0 flex-col'>
{sessionLoading || orgsLoading ? (
isCollapsed ? (
<>
{SKELETON_SECTIONS.map((count, sectionIdx) => (
<div key={sectionIdx} className='flex flex-col gap-[2px] px-[8px]'>
{Array.from({ length: count }, (_, i) => (
<div key={i} className='mx-[2px] flex h-[30px] items-center px-[8px]'>
<Skeleton className='h-[16px] w-[16px] rounded-[4px]' />
</div>
))}
</div>
))}
</>
) : (
Array.from({ length: 3 }, (_, i) => (
<div key={i} className='sidebar-collapse-hide flex flex-shrink-0 flex-col'>
<div className='px-[16px] pb-[6px]'>
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
</div>
@@ -220,73 +235,77 @@ export function SettingsSidebar({
</div>
</div>
))
: sectionConfig.map(({ key, title }) => {
const sectionItems = navigationItems.filter((item) => item.section === key)
if (sectionItems.length === 0) return null
)
) : (
sectionConfig.map(({ key, title }) => {
const sectionItems = navigationItems.filter((item) => item.section === key)
if (sectionItems.length === 0) return null
return (
<div key={key} className='flex flex-shrink-0 flex-col'>
<div className='px-[16px] pb-[6px]'>
return (
<div key={key} className='flex flex-shrink-0 flex-col'>
{!isCollapsed && (
<div className='sidebar-collapse-remove px-[16px] pb-[6px]'>
<div className='font-base text-[var(--text-icon)] text-small'>{title}</div>
</div>
<div className='flex flex-col gap-[2px] px-[8px]'>
{sectionItems.map((item) => {
const Icon = item.icon
const active = activeSection === item.id
const itemClassName = cn(
'group mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)]',
active && 'bg-[var(--surface-active)]'
)
const content = (
<>
<Icon className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate font-base text-[var(--text-body)]'>
{item.label}
</span>
</>
)
)}
<div className='flex flex-col gap-[2px] px-[8px]'>
{sectionItems.map((item) => {
const Icon = item.icon
const active = activeSection === item.id
const itemClassName = cn(
'group mx-[2px] flex h-[30px] items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--surface-active)]',
active && 'bg-[var(--surface-active)]'
)
const content = (
<>
<Icon className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate font-base text-[var(--text-body)]'>
{item.label}
</span>
</>
)
const element = item.externalUrl ? (
<a
href={item.externalUrl}
target='_blank'
rel='noopener noreferrer'
className={itemClassName}
>
{content}
</a>
) : (
<button
type='button'
className={itemClassName}
onMouseEnter={() => handlePrefetch(item.id)}
onFocus={() => handlePrefetch(item.id)}
onClick={() =>
router.replace(
getSettingsHref({ section: item.id as SettingsSection }),
{ scroll: false }
)
}
>
{content}
</button>
)
const element = item.externalUrl ? (
<a
href={item.externalUrl}
target='_blank'
rel='noopener noreferrer'
className={itemClassName}
>
{content}
</a>
) : (
<button
type='button'
className={itemClassName}
onMouseEnter={() => handlePrefetch(item.id)}
onFocus={() => handlePrefetch(item.id)}
onClick={() =>
router.replace(getSettingsHref({ section: item.id as SettingsSection }), {
scroll: false,
})
}
>
{content}
</button>
)
return (
<Tooltip.Root key={`${item.id}-${isCollapsed}`}>
<Tooltip.Trigger asChild>{element}</Tooltip.Trigger>
{showCollapsedContent && (
<Tooltip.Content side='right'>
<p>{item.label}</p>
</Tooltip.Content>
)}
</Tooltip.Root>
)
})}
</div>
return (
<Tooltip.Root key={`${item.id}-${isCollapsed}`}>
<Tooltip.Trigger asChild>{element}</Tooltip.Trigger>
{showCollapsedContent && (
<Tooltip.Content side='right'>
<p>{item.label}</p>
</Tooltip.Content>
)}
</Tooltip.Root>
)
})}
</div>
)
})}
</div>
)
})
)}
</div>
</>
)

View File

@@ -3,6 +3,7 @@
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { MoreHorizontal } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useParams, usePathname, useRouter } from 'next/navigation'
import {
@@ -65,6 +66,7 @@ import {
useImportWorkflow,
useImportWorkspace,
} from '@/app/workspace/[workspaceId]/w/hooks'
import { getBrandConfig } from '@/ee/whitelabeling'
import { useFolders } from '@/hooks/queries/folders'
import { useDeleteTask, useDeleteTasks, useRenameTask, useTasks } from '@/hooks/queries/tasks'
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -246,6 +248,7 @@ export const SIDEBAR_SCROLL_EVENT = 'sidebar-scroll-to-item'
* @returns Sidebar with workflows panel
*/
export const Sidebar = memo(function Sidebar() {
const brand = getBrandConfig()
const params = useParams()
const workspaceId = params.workspaceId as string
const workflowId = params.workflowId as string | undefined
@@ -966,7 +969,18 @@ export const Sidebar = memo(function Sidebar() {
className='group flex h-[30px] w-[30px] items-center justify-center rounded-[8px] hover:bg-[var(--surface-active)]'
aria-label='Expand sidebar'
>
<Sim className='h-[16px] w-[16px] text-[var(--text-icon)] group-hover:hidden' />
{brand.logoUrl ? (
<Image
src={brand.logoUrl}
alt={brand.name}
width={16}
height={16}
className='h-[16px] w-[16px] object-contain group-hover:hidden'
unoptimized
/>
) : (
<Sim className='h-[16px] w-[16px] text-[var(--text-icon)] group-hover:hidden' />
)}
<PanelLeft className='hidden h-[16px] w-[16px] rotate-180 text-[var(--text-icon)] group-hover:block' />
</button>
) : (
@@ -974,7 +988,18 @@ export const Sidebar = memo(function Sidebar() {
href={`/workspace/${workspaceId}/home`}
className='flex h-[30px] w-[30px] items-center justify-center rounded-[8px] hover:bg-[var(--surface-active)]'
>
<Sim className='h-[16px] w-[16px] text-[var(--text-icon)]' />
{brand.logoUrl ? (
<Image
src={brand.logoUrl}
alt={brand.name}
width={16}
height={16}
className='h-[16px] w-[16px] object-contain'
unoptimized
/>
) : (
<Sim className='h-[16px] w-[16px] text-[var(--text-icon)]' />
)}
</Link>
)}
</Tooltip.Trigger>

View File

@@ -3,30 +3,42 @@
* Colors are derived from globals.css light mode tokens.
*/
/** Color tokens from globals.css (light mode) */
export const colors = {
/** Main canvas background */
bgOuter: '#F7F9FC',
/** Card/container background - pure white */
bgCard: '#ffffff',
/** Primary text color */
textPrimary: '#2d2d2d',
/** Secondary text color */
textSecondary: '#404040',
/** Tertiary text color */
textTertiary: '#5c5c5c',
/** Muted text (footer) */
textMuted: '#737373',
/** Brand primary - purple */
brandPrimary: '#6f3dfa',
/** Brand tertiary - green (matches Run/Deploy buttons) */
brandTertiary: '#33C482',
/** Border/divider color */
divider: '#ededed',
/** Footer background */
footerBg: '#F7F9FC',
import { getBrandConfig } from '@/ee/whitelabeling'
/** Color tokens from globals.css (light mode), brand-aware for whitelabeled instances */
function buildColors() {
const brand = getBrandConfig()
const isWhitelabeled = brand.isWhitelabeled
const accentColor =
isWhitelabeled && brand.theme?.primaryColor ? brand.theme.primaryColor : '#33C482'
return {
/** Main canvas background */
bgOuter: '#F7F9FC',
/** Card/container background - pure white */
bgCard: '#ffffff',
/** Primary text color */
textPrimary: '#2d2d2d',
/** Secondary text color */
textSecondary: '#404040',
/** Tertiary text color */
textTertiary: '#5c5c5c',
/** Muted text (footer) */
textMuted: '#737373',
/** Brand primary - purple */
brandPrimary:
isWhitelabeled && brand.theme?.primaryColor ? brand.theme.primaryColor : '#6f3dfa',
/** Brand accent - used for buttons and links */
brandTertiary: accentColor,
/** Border/divider color */
divider: '#ededed',
/** Footer background */
footerBg: '#F7F9FC',
}
}
export const colors = buildColors()
/** Typography settings */
export const typography = {
fontFamily: "-apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica', sans-serif",

View File

@@ -28,6 +28,7 @@ export function EmailFooter({
showUnsubscribe = true,
}: EmailFooterProps) {
const brand = getBrandConfig()
const isWhitelabeled = brand.isWhitelabeled
const footerLinkStyle = {
color: colors.textMuted,
@@ -58,79 +59,87 @@ export function EmailFooter({
</td>
</tr>
{/* Social links row */}
<tr>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
<td>
<table cellPadding={0} cellSpacing={0} style={{ border: 0 }}>
<tbody>
<tr>
<td align='left' style={{ padding: '0 8px 0 0' }}>
<Link href={`${baseUrl}/x`} rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/x-icon.png`}
width='20'
height='20'
alt='X'
/>
</Link>
</td>
<td align='left' style={{ padding: '0 8px' }}>
<Link href={`${baseUrl}/discord`} rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/discord-icon.png`}
width='20'
height='20'
alt='Discord'
/>
</Link>
</td>
<td align='left' style={{ padding: '0 8px' }}>
<Link href={`${baseUrl}/github`} rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/github-icon.png`}
width='20'
height='20'
alt='GitHub'
/>
</Link>
</td>
</tr>
</tbody>
</table>
</td>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
</tr>
{/* Social links row — hidden for whitelabeled instances */}
{!isWhitelabeled && (
<>
<tr>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
<td>
<table cellPadding={0} cellSpacing={0} style={{ border: 0 }}>
<tbody>
<tr>
<td align='left' style={{ padding: '0 8px 0 0' }}>
<Link href={`${baseUrl}/x`} rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/x-icon.png`}
width='20'
height='20'
alt='X'
/>
</Link>
</td>
<td align='left' style={{ padding: '0 8px' }}>
<Link href={`${baseUrl}/discord`} rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/discord-icon.png`}
width='20'
height='20'
alt='Discord'
/>
</Link>
</td>
<td align='left' style={{ padding: '0 8px' }}>
<Link href={`${baseUrl}/github`} rel='noopener noreferrer'>
<Img
src={`${baseUrl}/static/github-icon.png`}
width='20'
height='20'
alt='GitHub'
/>
</Link>
</td>
</tr>
</tbody>
</table>
</td>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
</tr>
<tr>
<td style={baseStyles.spacer} height={16}>
&nbsp;
</td>
</tr>
<tr>
<td style={baseStyles.spacer} height={16}>
&nbsp;
</td>
</tr>
</>
)}
{/* Address row */}
<tr>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
<td style={baseStyles.footerText}>
{brand.name}
{isHosted && <>, 80 Langton St, San Francisco, CA 94103, USA</>}
</td>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
</tr>
{/* Address row — hidden for whitelabeled instances */}
{!isWhitelabeled && (
<>
<tr>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
<td style={baseStyles.footerText}>
{brand.name}
{isHosted && <>, 80 Langton St, San Francisco, CA 94103, USA</>}
</td>
<td style={baseStyles.gutter} width={spacing.gutter}>
&nbsp;
</td>
</tr>
<tr>
<td style={baseStyles.spacer} height={8}>
&nbsp;
</td>
</tr>
<tr>
<td style={baseStyles.spacer} height={8}>
&nbsp;
</td>
</tr>
</>
)}
{/* Contact row */}
<tr>

View File

@@ -56,6 +56,7 @@ export {
DropdownMenuTrigger,
} from './dropdown-menu/dropdown-menu'
export { Input, type InputProps, inputVariants } from './input/input'
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from './input-otp/input-otp'
export { Label } from './label/label'
export {
MODAL_SIZES,

View File

@@ -1,3 +1,29 @@
/**
* An OTP input component matching the emcn design system.
*
* Wraps the `input-otp` library with emcn design tokens for consistent styling.
*
* @example
* ```tsx
* import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/emcn'
*
* <InputOTP maxLength={6} value={otp} onChange={setOtp}>
* <InputOTPGroup>
* <InputOTPSlot index={0} />
* <InputOTPSlot index={1} />
* <InputOTPSlot index={2} />
* <InputOTPSlot index={3} />
* <InputOTPSlot index={4} />
* <InputOTPSlot index={5} />
* </InputOTPGroup>
* </InputOTP>
* ```
*
* @see InputOTP - Root component wrapping OTPInput
* @see InputOTPGroup - Groups slots together
* @see InputOTPSlot - Individual digit slot
* @see InputOTPSeparator - Visual separator between groups
*/
'use client'
import * as React from 'react'
@@ -5,6 +31,9 @@ import { OTPInput, OTPInputContext } from 'input-otp'
import { Minus } from 'lucide-react'
import { cn } from '@/lib/core/utils/cn'
/**
* Root OTP input component. Manages the overall input state and layout.
*/
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
@@ -21,14 +50,22 @@ const InputOTP = React.forwardRef<
))
InputOTP.displayName = 'InputOTP'
/**
* Groups OTP slots together with consistent spacing.
*/
const InputOTPGroup = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center', className)} {...props} />
<div ref={ref} className={cn('flex items-center gap-2', className)} {...props} />
))
InputOTPGroup.displayName = 'InputOTPGroup'
/**
* Individual OTP digit slot. Displays the entered character and a fake caret when active.
*
* Uses emcn design tokens for consistent styling with the Input component.
*/
const InputOTPSlot = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'> & { index: number }
@@ -40,8 +77,8 @@ const InputOTPSlot = React.forwardRef<
<div
ref={ref}
className={cn(
'relative flex h-12 w-12 items-center justify-center border-input border-y border-r font-semibold text-lg shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md',
isActive && 'z-10 ring-2 ring-primary ring-offset-1',
'relative flex h-12 w-12 items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] font-medium text-[var(--text-primary)] text-lg transition-colors',
isActive && 'z-10 border-[var(--text-muted)] ring-1 ring-[var(--text-muted)]',
className
)}
{...props}
@@ -49,7 +86,7 @@ const InputOTPSlot = React.forwardRef<
{char}
{hasFakeCaret && (
<div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
<div className='h-6 w-px animate-caret-blink bg-primary duration-1000' />
<div className='h-6 w-px animate-caret-blink bg-[var(--text-primary)] duration-1000' />
</div>
)}
</div>
@@ -57,11 +94,14 @@ const InputOTPSlot = React.forwardRef<
})
InputOTPSlot.displayName = 'InputOTPSlot'
/**
* Visual separator between OTP slot groups.
*/
const InputOTPSeparator = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ ...props }, ref) => (
<div ref={ref} role='separator' className='text-muted-foreground' {...props}>
<div ref={ref} role='separator' className='text-[var(--text-muted)]' {...props}>
<Minus />
</div>
))

View File

@@ -1,50 +0,0 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/core/utils/cn'
const alertVariants = cva(
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role='alert' className={cn(alertVariants({ variant }), className)} {...props} />
))
Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, children, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
>
{children}
</h5>
)
)
AlertTitle.displayName = 'AlertTitle'
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
))
AlertDescription.displayName = 'AlertDescription'
export { Alert, AlertTitle, AlertDescription }

View File

@@ -1,142 +0,0 @@
'use client'
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { cn } from '@/lib/core/utils/cn'
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, style, ...props }, ref) => {
return (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[10000000] bg-white/50 data-[state=closed]:animate-out data-[state=open]:animate-in dark:bg-black/50',
className
)}
style={{ backdropFilter: 'blur(1.5px)', ...style }}
{...props}
/>
)
})
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
hideCloseButton?: boolean
}
>(({ className, children, hideCloseButton = false, ...props }, ref) => {
const [isInteractionReady, setIsInteractionReady] = React.useState(false)
React.useEffect(() => {
// Prevent rapid interactions that can cause instability
const timer = setTimeout(() => setIsInteractionReady(true), 100)
return () => clearTimeout(timer)
}, [])
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-[10000000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-[8px] border border-[var(--border-muted)] bg-[var(--surface-3)] p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in dark:bg-[var(--surface-3)]',
className
)}
onEscapeKeyDown={(e) => {
// Prevent escape during rapid interactions
if (!isInteractionReady) {
e.preventDefault()
return
}
// Allow escape but prevent event bubbling issues
e.stopPropagation()
}}
onPointerDown={(e) => {
// Prevent event bubbling that might interfere with parent hover states
e.stopPropagation()
}}
onPointerUp={(e) => {
// Prevent event bubbling that might interfere with parent hover states
e.stopPropagation()
}}
{...props}
>
<DialogPrimitive.Title>Dialog</DialogPrimitive.Title>
{children}
{!hideCloseButton && (
<DialogPrimitive.Close
className='absolute top-4 right-4 h-4 w-4 p-0 text-muted-foreground transition-colors hover:bg-transparent hover:text-foreground focus:outline-none disabled:pointer-events-none'
disabled={!isInteractionReady}
tabIndex={-1}
>
<X className='h-4 w-4' />
<span className='sr-only'>Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
})
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
)
DialogHeader.displayName = 'DialogHeader'
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
)
DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('font-medium text-lg leading-none tracking-tight', className)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -1,19 +1,5 @@
export { Alert, AlertDescription, AlertTitle } from './alert'
export { Button, buttonVariants } from './button'
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
} from './dialog'
export { Input } from './input'
export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from './input-otp'
export { Label } from './label'
export { Progress } from './progress'
export { SearchHighlight } from './search-highlight'

View File

@@ -21,21 +21,20 @@ export function ContactButton({ href, children }: ContactButtonProps) {
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
borderRadius: '10px',
background: 'linear-gradient(to bottom, #8357ff, #6f3dfa)',
border: '1px solid #6f3dfa',
boxShadow: 'inset 0 2px 4px 0 #9b77ff',
paddingTop: '6px',
paddingBottom: '6px',
paddingLeft: '12px',
paddingRight: '10px',
fontSize: '15px',
fontWeight: 500,
color: '#ffffff',
gap: '7px',
borderRadius: '5px',
background: '#FFFFFF',
border: '1px solid #FFFFFF',
paddingTop: '5px',
paddingBottom: '5px',
paddingLeft: '9px',
paddingRight: '9px',
fontSize: '13.5px',
fontWeight: 430,
color: '#000000',
textDecoration: 'none',
opacity: isHovered ? 0.9 : 1,
transition: 'opacity 200ms',
transition: 'background 200ms, border-color 200ms',
...(isHovered ? { background: '#E0E0E0', borderColor: '#E0E0E0' } : {}),
}}
>
{children}

View File

@@ -3,16 +3,13 @@
import { type KeyboardEvent, useState } from 'react'
import { createLogger } from '@sim/logger'
import { useRouter } from 'next/navigation'
import { Input } from '@/components/emcn'
import { Label } from '@/components/ui/label'
import { Input, Label } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import AuthBackground from '@/app/(auth)/components/auth-background'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
import { SupportFooter } from '@/app/(auth)/components/support-footer'
import Nav from '@/app/(landing)/components/nav/nav'
import Navbar from '@/app/(home)/components/navbar/navbar'
const logger = createLogger('SSOAuth')
@@ -99,19 +96,19 @@ export default function SSOAuth({ identifier }: SSOAuthProps) {
}
return (
<AuthBackground>
<main className='relative flex min-h-screen flex-col text-foreground'>
<Nav hideAuthButtons={true} variant='auth' />
<AuthBackground className='dark font-[430] font-season'>
<main className='relative flex min-h-screen flex-col text-[#ECECEC]'>
<header>
<Navbar logoOnly />
</header>
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
<div className='w-full max-w-lg px-4'>
<div className='flex flex-col items-center justify-center'>
<div className='space-y-1 text-center'>
<h1
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
>
<h1 className='font-[500] text-[#ECECEC] text-[32px] tracking-tight'>
SSO Authentication
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
<p className='font-[380] text-[#999] text-[16px]'>
This chat requires SSO authentication
</p>
</div>
@@ -121,7 +118,7 @@ export default function SSOAuth({ identifier }: SSOAuthProps) {
e.preventDefault()
handleAuthenticate()
}}
className={`${inter.className} mt-8 w-full max-w-[410px] space-y-6`}
className='mt-8 w-full max-w-[410px] space-y-6'
>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -140,10 +137,9 @@ export default function SSOAuth({ identifier }: SSOAuthProps) {
onChange={handleEmailChange}
onKeyDown={handleKeyDown}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showEmailValidationError &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
'border-red-500 focus:border-red-500'
)}
autoFocus
/>

View File

@@ -4,16 +4,12 @@ import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button, Input, Label } from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import { env, isFalsy } from '@/lib/core/config/env'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
import { BrandedButton } from '@/app/(auth)/components/branded-button'
const logger = createLogger('SSOForm')
@@ -58,7 +54,6 @@ export default function SSOForm() {
const [email, setEmail] = useState('')
const [emailErrors, setEmailErrors] = useState<string[]>([])
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
const buttonClass = useBrandedButtonClass()
const [callbackUrl, setCallbackUrl] = useState('/workspace')
useEffect(() => {
@@ -156,15 +151,11 @@ export default function SSOForm() {
return (
<>
<div className='space-y-1 text-center'>
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
Sign in with SSO
</h1>
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
Enter your work email to continue
</p>
<h1 className={'font-[500] text-[#ECECEC] text-[32px] tracking-tight'}>Sign in with SSO</h1>
<p className={'font-[380] text-[#999] text-[16px]'}>Enter your work email to continue</p>
</div>
<form onSubmit={onSubmit} className={`${inter.className} mt-8 space-y-8`}>
<form onSubmit={onSubmit} className={'mt-8 space-y-8'}>
<div className='space-y-6'>
<div className='space-y-2'>
<div className='flex items-center justify-between'>
@@ -182,7 +173,6 @@ export default function SSOForm() {
value={email}
onChange={handleEmailChange}
className={cn(
'rounded-[10px] shadow-sm transition-colors focus:border-gray-400 focus:ring-2 focus:ring-gray-100',
showEmailValidationError &&
emailErrors.length > 0 &&
'border-red-500 focus:border-red-500 focus:ring-red-100 focus-visible:ring-red-500'
@@ -198,36 +188,33 @@ export default function SSOForm() {
</div>
</div>
<Button
<BrandedButton
type='submit'
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
disabled={isLoading}
loading={isLoading}
loadingText='Redirecting to SSO provider'
>
{isLoading ? 'Redirecting to SSO provider...' : 'Continue with SSO'}
</Button>
Continue with SSO
</BrandedButton>
</form>
{/* Only show divider and email signin button if email/password is enabled */}
{!isFalsy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && (
<>
<div className={`${inter.className} relative my-6 font-light`}>
<div className='relative my-6 font-light'>
<div className='absolute inset-0 flex items-center'>
<div className='auth-divider w-full border-t' />
<div className='w-full border-[#2A2A2A] border-t' />
</div>
<div className='relative flex justify-center text-sm'>
<span className='bg-white px-4 font-[340] text-muted-foreground'>Or</span>
<span className='bg-[#1C1C1C] px-4 font-[340] text-[#999]'>Or</span>
</div>
</div>
<div className={`${inter.className} space-y-3`}>
<div className='space-y-3'>
<Link
href={`/login${callbackUrl ? `?callbackUrl=${encodeURIComponent(callbackUrl)}` : ''}`}
>
<Button
variant='outline'
className='w-full rounded-[10px] shadow-sm hover:bg-gray-50'
type='button'
>
<Button variant='outline' className='w-full rounded-[10px]' type='button'>
Sign in with email
</Button>
</Link>
@@ -237,26 +224,24 @@ export default function SSOForm() {
{/* Only show signup link if email/password signup is enabled */}
{!isFalsy(env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED) && (
<div className={`${inter.className} pt-6 text-center font-light text-[15px]`}>
<div className='pt-6 text-center font-light text-[15px]'>
<span className='font-normal'>Don't have an account? </span>
<Link
href={`/signup${callbackUrl ? `?callbackUrl=${encodeURIComponent(callbackUrl)}` : ''}`}
className='font-medium text-[var(--brand-accent-hex)] underline-offset-4 transition hover:text-[var(--brand-accent-hover-hex)] hover:underline'
className='font-medium text-[#ECECEC] underline-offset-4 transition hover:text-white hover:underline'
>
Sign up
</Link>
</div>
)}
<div
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[14px] leading-relaxed sm:px-8 md:px-[44px]`}
>
<div className='absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[#999] text-[14px] leading-relaxed sm:px-8 md:px-[44px]'>
By signing in, you agree to our{' '}
<Link
href='/terms'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Terms of Service
</Link>{' '}
@@ -265,7 +250,7 @@ export default function SSOForm() {
href='/privacy'
target='_blank'
rel='noopener noreferrer'
className='auth-link underline-offset-4 transition hover:underline'
className='text-[#999] underline-offset-4 transition hover:text-[#ECECEC] hover:underline'
>
Privacy Policy
</Link>

View File

@@ -23,6 +23,12 @@ const getThemeColors = (): ThemeColors => {
* Supports runtime configuration via Docker/Kubernetes
*/
export const getBrandConfig = (): BrandConfig => {
const hasCustomBrand = Boolean(
getEnv('NEXT_PUBLIC_BRAND_NAME') ||
getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') ||
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR')
)
return {
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultBrandConfig.name,
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultBrandConfig.logoUrl,
@@ -34,6 +40,7 @@ export const getBrandConfig = (): BrandConfig => {
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultBrandConfig.termsUrl,
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultBrandConfig.privacyUrl,
theme: getThemeColors(),
isWhitelabeled: hasCustomBrand,
}
}

View File

@@ -15,6 +15,8 @@ export function generateThemeCSS(): string {
if (process.env.NEXT_PUBLIC_BRAND_PRIMARY_COLOR) {
cssVars.push(`--brand-primary-hex: ${process.env.NEXT_PUBLIC_BRAND_PRIMARY_COLOR};`)
// Override tertiary-2 so Run/Deploy buttons and other tertiary-styled elements use the brand color
cssVars.push(`--brand-tertiary-2: ${process.env.NEXT_PUBLIC_BRAND_PRIMARY_COLOR};`)
}
if (process.env.NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR) {

View File

@@ -2,12 +2,12 @@ export function FAQ({ items }: { items: { q: string; a: string }[] }) {
if (!items || items.length === 0) return null
return (
<section className='mt-12'>
<h2 className='mb-4 font-medium text-[24px]'>FAQ</h2>
<h2 className='mb-4 font-medium text-[#ECECEC] text-[24px]'>FAQ</h2>
<div className='space-y-6'>
{items.map((it, i) => (
<div key={i}>
<h3 className='mb-2 font-medium text-[20px]'>{it.q}</h3>
<p className='text-[19px] text-gray-800 leading-relaxed'>{it.a}</p>
<h3 className='mb-2 font-medium text-[#ECECEC] text-[20px]'>{it.q}</h3>
<p className='text-[#999] text-[19px] leading-relaxed'>{it.a}</p>
</div>
))}
</div>

View File

@@ -20,7 +20,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
<h2
{...props}
style={{ fontSize: '30px', marginTop: '3rem', marginBottom: '1.5rem' }}
className={clsx('font-medium text-black leading-tight', className)}
className={clsx('font-medium text-[#ECECEC] leading-tight', className)}
>
{children}
</h2>
@@ -29,7 +29,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
<h3
{...props}
style={{ fontSize: '24px', marginTop: '1.5rem', marginBottom: '0.75rem' }}
className={clsx('font-medium leading-tight', className)}
className={clsx('font-medium text-[#ECECEC] leading-tight', className)}
>
{children}
</h3>
@@ -38,7 +38,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
<h4
{...props}
style={{ fontSize: '19px', marginTop: '1.5rem', marginBottom: '0.75rem' }}
className={clsx('font-medium leading-tight', className)}
className={clsx('font-medium text-[#ECECEC] leading-tight', className)}
>
{children}
</h4>
@@ -47,14 +47,14 @@ export const mdxComponents: MDXRemoteProps['components'] = {
<p
{...props}
style={{ fontSize: '19px', marginBottom: '1.5rem', fontWeight: '400' }}
className={clsx('text-gray-800 leading-relaxed', props.className)}
className={clsx('text-[#999] leading-relaxed', props.className)}
/>
),
ul: (props: any) => (
<ul
{...props}
style={{ fontSize: '19px', marginBottom: '1rem', fontWeight: '400' }}
className={clsx('list-outside list-disc pl-6 text-gray-800 leading-relaxed', props.className)}
className={clsx('list-outside list-disc pl-6 text-[#999] leading-relaxed', props.className)}
/>
),
ol: (props: any) => (
@@ -62,14 +62,16 @@ export const mdxComponents: MDXRemoteProps['components'] = {
{...props}
style={{ fontSize: '19px', marginBottom: '1rem', fontWeight: '400' }}
className={clsx(
'list-outside list-decimal pl-6 text-gray-800 leading-relaxed',
'list-outside list-decimal pl-6 text-[#999] leading-relaxed',
props.className
)}
/>
),
li: (props: any) => <li {...props} className={clsx('mb-1', props.className)} />,
strong: (props: any) => <strong {...props} className={clsx('font-semibold', props.className)} />,
em: (props: any) => <em {...props} className={clsx('italic', props.className)} />,
strong: (props: any) => (
<strong {...props} className={clsx('font-semibold text-[#ECECEC]', props.className)} />
),
em: (props: any) => <em {...props} className={clsx('text-[#bbb] italic', props.className)} />,
a: (props: any) => {
const isAnchorLink = props.className?.includes('anchor')
if (isAnchorLink) {
@@ -78,10 +80,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
return (
<a
{...props}
className={clsx(
'font-medium text-[#33B4FF] underline hover:text-[#2A9FE8]',
props.className
)}
className={clsx('font-medium text-[#ECECEC] underline hover:text-white', props.className)}
/>
)
},
@@ -91,7 +90,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
hr: (props: any) => (
<hr
{...props}
className={clsx('my-8 border-gray-200', props.className)}
className={clsx('my-8 border-[#2A2A2A]', props.className)}
style={{ marginBottom: '1.5rem' }}
/>
),
@@ -135,7 +134,7 @@ export const mdxComponents: MDXRemoteProps['components'] = {
<code
{...props}
className={clsx(
'rounded bg-gray-100 px-1.5 py-0.5 font-mono font-normal text-[0.9em] text-red-600',
'rounded bg-[#2A2A2A] px-1.5 py-0.5 font-mono font-normal text-[#ECECEC] text-[0.9em]',
props.className
)}
style={{ fontWeight: 400 }}

View File

@@ -19,4 +19,5 @@ export const defaultBrandConfig: BrandConfig = {
accentHoverColor: '#a66fff',
backgroundColor: '#0c0c0c',
},
isWhitelabeled: false,
}

View File

@@ -16,4 +16,6 @@ export interface BrandConfig {
termsUrl?: string
privacyUrl?: string
theme?: ThemeColors
/** Whether this instance has custom branding applied (any brand env var is set) */
isWhitelabeled: boolean
}