mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
10 Commits
fix/side
...
cursor/cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5d1575b3 | ||
|
|
a814db77b8 | ||
|
|
84b8604f7c | ||
|
|
4ede522671 | ||
|
|
0725c98afa | ||
|
|
c191f15501 | ||
|
|
03739d0352 | ||
|
|
a58de5a24b | ||
|
|
af872df5f8 | ||
|
|
71afa3a87c |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'>✓</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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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're looking for doesn'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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]'>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}>
|
||||
|
||||
</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}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{/* Social links row — hidden for whitelabeled instances */}
|
||||
{!isWhitelabeled && (
|
||||
<>
|
||||
<tr>
|
||||
<td style={baseStyles.gutter} width={spacing.gutter}>
|
||||
|
||||
</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}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style={baseStyles.spacer} height={16}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={baseStyles.spacer} height={16}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Address row */}
|
||||
<tr>
|
||||
<td style={baseStyles.gutter} width={spacing.gutter}>
|
||||
|
||||
</td>
|
||||
<td style={baseStyles.footerText}>
|
||||
{brand.name}
|
||||
{isHosted && <>, 80 Langton St, San Francisco, CA 94103, USA</>}
|
||||
</td>
|
||||
<td style={baseStyles.gutter} width={spacing.gutter}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{/* Address row — hidden for whitelabeled instances */}
|
||||
{!isWhitelabeled && (
|
||||
<>
|
||||
<tr>
|
||||
<td style={baseStyles.gutter} width={spacing.gutter}>
|
||||
|
||||
</td>
|
||||
<td style={baseStyles.footerText}>
|
||||
{brand.name}
|
||||
{isHosted && <>, 80 Langton St, San Francisco, CA 94103, USA</>}
|
||||
</td>
|
||||
<td style={baseStyles.gutter} width={spacing.gutter}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style={baseStyles.spacer} height={8}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={baseStyles.spacer} height={8}>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Contact row */}
|
||||
<tr>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -19,4 +19,5 @@ export const defaultBrandConfig: BrandConfig = {
|
||||
accentHoverColor: '#a66fff',
|
||||
backgroundColor: '#0c0c0c',
|
||||
},
|
||||
isWhitelabeled: false,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user