Skip to content

Commit 303fd24

Browse files
committed
styling consolidation, finishing touches on form
1 parent 73ced91 commit 303fd24

File tree

56 files changed

+849
-937
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+849
-937
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use client'
2+
3+
import { forwardRef, useState } from 'react'
4+
import { ArrowRight, ChevronRight, Loader2 } from 'lucide-react'
5+
import { Button, type ButtonProps as EmcnButtonProps } from '@/components/emcn'
6+
import { cn } from '@/lib/core/utils/cn'
7+
import { useCtaButtonClass } from '@/hooks/use-cta-button-class'
8+
9+
export interface CTAButtonProps extends Omit<EmcnButtonProps, 'variant' | 'size'> {
10+
/** Shows loading spinner and disables button */
11+
loading?: boolean
12+
/** Text to show when loading (appends "..." automatically) */
13+
loadingText?: string
14+
/** Show arrow animation on hover (default: true) */
15+
showArrow?: boolean
16+
/** Make button full width (default: true) */
17+
fullWidth?: boolean
18+
}
19+
20+
/**
21+
* Branded CTA button for auth and status pages.
22+
* Automatically detects whitelabel customization and applies appropriate styling.
23+
*
24+
* @example
25+
* ```tsx
26+
* // Primary branded button with arrow
27+
* <CTAButton onClick={handleSubmit}>Sign In</CTAButton>
28+
*
29+
* // Loading state
30+
* <CTAButton loading loadingText="Signing in">Sign In</CTAButton>
31+
*
32+
* // Without arrow animation
33+
* <CTAButton showArrow={false}>Continue</CTAButton>
34+
* ```
35+
*/
36+
export const CTAButton = forwardRef<HTMLButtonElement, CTAButtonProps>(
37+
(
38+
{
39+
children,
40+
loading = false,
41+
loadingText,
42+
showArrow = true,
43+
fullWidth = true,
44+
className,
45+
disabled,
46+
onMouseEnter,
47+
onMouseLeave,
48+
...props
49+
},
50+
ref
51+
) => {
52+
const buttonClass = useCtaButtonClass()
53+
const [isHovered, setIsHovered] = useState(false)
54+
55+
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
56+
setIsHovered(true)
57+
onMouseEnter?.(e)
58+
}
59+
60+
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
61+
setIsHovered(false)
62+
onMouseLeave?.(e)
63+
}
64+
65+
return (
66+
<Button
67+
ref={ref}
68+
variant='cta'
69+
size='cta'
70+
disabled={disabled || loading}
71+
onMouseEnter={handleMouseEnter}
72+
onMouseLeave={handleMouseLeave}
73+
className={cn(buttonClass, 'group', fullWidth && 'w-full', className)}
74+
{...props}
75+
>
76+
{loading ? (
77+
<span className='flex items-center gap-2'>
78+
<Loader2 className='h-4 w-4 animate-spin' />
79+
{loadingText ? `${loadingText}...` : children}
80+
</span>
81+
) : showArrow ? (
82+
<span className='flex items-center gap-1'>
83+
{children}
84+
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
85+
{isHovered ? (
86+
<ArrowRight className='h-4 w-4' aria-hidden='true' />
87+
) : (
88+
<ChevronRight className='h-4 w-4' aria-hidden='true' />
89+
)}
90+
</span>
91+
</span>
92+
) : (
93+
children
94+
)}
95+
</Button>
96+
)
97+
}
98+
)
99+
100+
CTAButton.displayName = 'CTAButton'

apps/sim/app/(auth)/components/sso-login-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function SSOLoginButton({
3434
}
3535

3636
const primaryBtnClasses = cn(
37-
primaryClassName || 'auth-button-gradient',
37+
primaryClassName || 'cta-button-gradient',
3838
'flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200'
3939
)
4040

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client'
2+
3+
import type { ReactNode } from 'react'
4+
import { inter } from '@/app/_styles/fonts/inter/inter'
5+
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
6+
import AuthBackground from '@/app/(auth)/components/auth-background'
7+
import Nav from '@/app/(landing)/components/nav/nav'
8+
import { SupportFooter } from './support-footer'
9+
10+
export interface StatusPageLayoutProps {
11+
/** Page title displayed prominently */
12+
title: string
13+
/** Description text below the title */
14+
description: string | ReactNode
15+
/** Content to render below the title/description (usually buttons) */
16+
children?: ReactNode
17+
/** Whether to show the support footer (default: true) */
18+
showSupportFooter?: boolean
19+
/** Whether to hide the nav bar (useful for embedded forms) */
20+
hideNav?: boolean
21+
}
22+
23+
/**
24+
* Unified layout for status/error pages (404, form unavailable, chat error, etc.).
25+
* Uses AuthBackground and Nav for consistent styling with auth pages.
26+
*
27+
* @example
28+
* ```tsx
29+
* <StatusPageLayout
30+
* title="Page Not Found"
31+
* description="The page you're looking for doesn't exist."
32+
* >
33+
* <CTAButton onClick={() => router.push('/')}>Return to Home</CTAButton>
34+
* </StatusPageLayout>
35+
* ```
36+
*/
37+
export function StatusPageLayout({
38+
title,
39+
description,
40+
children,
41+
showSupportFooter = true,
42+
hideNav = false,
43+
}: StatusPageLayoutProps) {
44+
return (
45+
<AuthBackground>
46+
<main className='relative flex min-h-screen flex-col text-foreground'>
47+
{!hideNav && <Nav hideAuthButtons={true} variant='auth' />}
48+
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
49+
<div className='w-full max-w-lg px-4'>
50+
<div className='flex flex-col items-center justify-center'>
51+
<div className='space-y-1 text-center'>
52+
<h1
53+
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
54+
>
55+
{title}
56+
</h1>
57+
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
58+
{description}
59+
</p>
60+
</div>
61+
62+
{children && (
63+
<div className={`${inter.className} mt-8 w-full max-w-[410px] space-y-3`}>
64+
{children}
65+
</div>
66+
)}
67+
</div>
68+
</div>
69+
</div>
70+
{showSupportFooter && <SupportFooter position='absolute' />}
71+
</main>
72+
</AuthBackground>
73+
)
74+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use client'
2+
3+
import { Loader2 } from 'lucide-react'
4+
import { inter } from '@/app/_styles/fonts/inter/inter'
5+
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
6+
import AuthBackground from '@/app/(auth)/components/auth-background'
7+
import Nav from '@/app/(landing)/components/nav/nav'
8+
import { SupportFooter } from './support-footer'
9+
10+
export interface StatusPageLoadingProps {
11+
/** Title to show while loading (default: "Loading") */
12+
title?: string
13+
/** Description text below the title */
14+
description?: string
15+
}
16+
17+
/**
18+
* Loading state component for status pages.
19+
* Displays a spinner with optional title and description.
20+
*
21+
* @example
22+
* ```tsx
23+
* <StatusPageLoading description="Loading your workspace..." />
24+
* ```
25+
*/
26+
export function StatusPageLoading({
27+
title = 'Loading',
28+
description = 'Please wait...',
29+
}: StatusPageLoadingProps) {
30+
return (
31+
<AuthBackground>
32+
<main className='relative flex min-h-screen flex-col text-foreground'>
33+
<Nav hideAuthButtons={true} variant='auth' />
34+
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
35+
<div className='w-full max-w-lg px-4'>
36+
<div className='flex flex-col items-center justify-center'>
37+
<div className='space-y-1 text-center'>
38+
<h1
39+
className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}
40+
>
41+
{title}
42+
</h1>
43+
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
44+
{description}
45+
</p>
46+
</div>
47+
48+
<div
49+
className={`${inter.className} mt-8 flex w-full items-center justify-center py-8`}
50+
>
51+
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
52+
</div>
53+
</div>
54+
</div>
55+
</div>
56+
<SupportFooter position='absolute' />
57+
</main>
58+
</AuthBackground>
59+
)
60+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use client'
2+
3+
import { useBrandConfig } from '@/lib/branding/branding'
4+
import { inter } from '@/app/_styles/fonts/inter/inter'
5+
6+
export interface SupportFooterProps {
7+
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */
8+
position?: 'fixed' | 'absolute'
9+
}
10+
11+
/**
12+
* Support footer component for auth and status pages.
13+
* Displays a "Need help? Contact support" link using branded support email.
14+
*
15+
* @example
16+
* ```tsx
17+
* // Fixed position (for standalone pages)
18+
* <SupportFooter />
19+
*
20+
* // Absolute position (for pages using AuthLayout)
21+
* <SupportFooter position="absolute" />
22+
* ```
23+
*/
24+
export function SupportFooter({ position = 'fixed' }: SupportFooterProps) {
25+
const brandConfig = useBrandConfig()
26+
27+
return (
28+
<div
29+
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}`}
30+
>
31+
Need help?{' '}
32+
<a
33+
href={`mailto:${brandConfig.supportEmail}`}
34+
className='auth-link underline-offset-4 transition hover:underline'
35+
>
36+
Contact support
37+
</a>
38+
</div>
39+
)
40+
}

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export default function LoginPage({
105105
const [password, setPassword] = useState('')
106106
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
107107
const [showValidationError, setShowValidationError] = useState(false)
108-
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
108+
const [buttonClass, setButtonClass] = useState('cta-button-gradient')
109109
const [isButtonHovered, setIsButtonHovered] = useState(false)
110110

111111
const [callbackUrl, setCallbackUrl] = useState('/workspace')
@@ -146,9 +146,9 @@ export default function LoginPage({
146146
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
147147

148148
if (brandAccent && brandAccent !== '#6f3dfa') {
149-
setButtonClass('auth-button-custom')
149+
setButtonClass('cta-button-custom')
150150
} else {
151-
setButtonClass('auth-button-gradient')
151+
setButtonClass('cta-button-gradient')
152152
}
153153
}
154154

apps/sim/app/(auth)/reset-password/reset-password-form.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function RequestResetForm({
2727
statusMessage,
2828
className,
2929
}: RequestResetFormProps) {
30-
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
30+
const [buttonClass, setButtonClass] = useState('cta-button-gradient')
3131
const [isButtonHovered, setIsButtonHovered] = useState(false)
3232

3333
useEffect(() => {
@@ -36,9 +36,9 @@ export function RequestResetForm({
3636
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
3737

3838
if (brandAccent && brandAccent !== '#6f3dfa') {
39-
setButtonClass('auth-button-custom')
39+
setButtonClass('cta-button-custom')
4040
} else {
41-
setButtonClass('auth-button-gradient')
41+
setButtonClass('cta-button-gradient')
4242
}
4343
}
4444

@@ -138,7 +138,7 @@ export function SetNewPasswordForm({
138138
const [validationMessage, setValidationMessage] = useState('')
139139
const [showPassword, setShowPassword] = useState(false)
140140
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
141-
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
141+
const [buttonClass, setButtonClass] = useState('cta-button-gradient')
142142
const [isButtonHovered, setIsButtonHovered] = useState(false)
143143

144144
useEffect(() => {
@@ -147,9 +147,9 @@ export function SetNewPasswordForm({
147147
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
148148

149149
if (brandAccent && brandAccent !== '#6f3dfa') {
150-
setButtonClass('auth-button-custom')
150+
setButtonClass('cta-button-custom')
151151
} else {
152-
setButtonClass('auth-button-gradient')
152+
setButtonClass('cta-button-gradient')
153153
}
154154
}
155155

apps/sim/app/(auth)/signup/signup-form.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function SignupFormContent({
9595
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
9696
const [redirectUrl, setRedirectUrl] = useState('')
9797
const [isInviteFlow, setIsInviteFlow] = useState(false)
98-
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
98+
const [buttonClass, setButtonClass] = useState('cta-button-gradient')
9999
const [isButtonHovered, setIsButtonHovered] = useState(false)
100100

101101
const [name, setName] = useState('')
@@ -128,9 +128,9 @@ function SignupFormContent({
128128
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
129129

130130
if (brandAccent && brandAccent !== '#6f3dfa') {
131-
setButtonClass('auth-button-custom')
131+
setButtonClass('cta-button-custom')
132132
} else {
133-
setButtonClass('auth-button-gradient')
133+
setButtonClass('cta-button-gradient')
134134
}
135135
}
136136

apps/sim/app/(auth)/sso/sso-form.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default function SSOForm() {
5757
const [email, setEmail] = useState('')
5858
const [emailErrors, setEmailErrors] = useState<string[]>([])
5959
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
60-
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
60+
const [buttonClass, setButtonClass] = useState('cta-button-gradient')
6161
const [callbackUrl, setCallbackUrl] = useState('/workspace')
6262

6363
useEffect(() => {
@@ -96,9 +96,9 @@ export default function SSOForm() {
9696
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
9797

9898
if (brandAccent && brandAccent !== '#6f3dfa') {
99-
setButtonClass('auth-button-custom')
99+
setButtonClass('cta-button-custom')
100100
} else {
101-
setButtonClass('auth-button-gradient')
101+
setButtonClass('cta-button-gradient')
102102
}
103103
}
104104

0 commit comments

Comments
 (0)