chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
/** Shared className for inline auth action links. */
|
||||
export const AUTH_TEXT_LINK =
|
||||
'font-medium text-[var(--brand-accent)] underline-offset-4 transition hover:text-[var(--brand-accent-hover)] hover:underline disabled:cursor-not-allowed disabled:opacity-50' as const
|
||||
@@ -0,0 +1,21 @@
|
||||
interface AuthDividerProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Or continue with" rule separating the email/password form from the
|
||||
* social/SSO options. Light tokens only: a `--border` hairline with the label
|
||||
* knocked out over the `--bg` canvas in `--text-muted`.
|
||||
*/
|
||||
export function AuthDivider({ label }: AuthDividerProps) {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className='absolute inset-0 flex items-center'>
|
||||
<div className='w-full border-[var(--border)] border-t' />
|
||||
</div>
|
||||
<div className='relative flex justify-center'>
|
||||
<span className='bg-[var(--bg)] px-4 text-[var(--text-muted)] text-sm'>{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Label } from '@sim/emcn'
|
||||
|
||||
interface AuthFieldProps {
|
||||
/** Matches the `id` set on the control rendered as {@link children}. */
|
||||
htmlFor: string
|
||||
label: string
|
||||
/** Validation messages to render beneath the control. */
|
||||
errors?: string[]
|
||||
/** Optional right-aligned action shown next to the label (e.g. Forgot password). */
|
||||
action?: ReactNode
|
||||
/** The field control — a {@link ChipInput}/{@link PasswordInput}. */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* A labeled form field row: canonical {@link Label}, an optional inline label
|
||||
* action, the control, and a validation-message list in the error token. The
|
||||
* control drives its own invalid chrome through its `error` prop — this wrapper
|
||||
* only owns the label row and the message list, so every auth field reads and
|
||||
* spaces identically.
|
||||
*/
|
||||
export function AuthField({ htmlFor, label, errors, action, children }: AuthFieldProps) {
|
||||
const hasErrors = Boolean(errors && errors.length > 0)
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label htmlFor={htmlFor}>{label}</Label>
|
||||
{action}
|
||||
</div>
|
||||
{children}
|
||||
{hasErrors && (
|
||||
<div className='space-y-1 text-[var(--text-error)] text-caption' aria-live='polite'>
|
||||
{errors?.map((error) => (
|
||||
<p key={error}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
|
||||
interface AuthFormMessageProps {
|
||||
type: 'error' | 'success'
|
||||
align?: 'left' | 'center'
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Form-level status copy (not tied to a single field) in the canonical tokens:
|
||||
* errors in `--text-error`, success in `--brand-accent`. One place owns the
|
||||
* auth message chrome so success/error states never drift to ad-hoc hex or
|
||||
* `text-red-*`/`#4CAF50` colors.
|
||||
*/
|
||||
export function AuthFormMessage({ type, align = 'left', children }: AuthFormMessageProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'space-y-1 text-caption',
|
||||
align === 'center' && 'text-center',
|
||||
type === 'error' ? 'text-[var(--text-error)]' : 'text-[var(--brand-accent)]'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface AuthHeaderProps {
|
||||
title: string
|
||||
description: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* The centered heading + subcopy block shared by every auth page and status
|
||||
* page. One source of truth for auth heading typography (light tokens, normal
|
||||
* weight, no bespoke tracking — aligned with the landing scale, sized down for
|
||||
* the single-column form).
|
||||
*/
|
||||
export function AuthHeader({ title, description }: AuthHeaderProps) {
|
||||
return (
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='text-balance text-[32px] text-[var(--text-primary)] leading-[1.2]'>{title}</h1>
|
||||
<p className='text-[var(--text-muted)] text-base leading-[1.5]'>{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { ChipInput, type ChipInputProps, cn } from '@sim/emcn'
|
||||
import { AUTH_CONTROL_HEIGHT } from '@/app/(auth)/components/constants'
|
||||
|
||||
/**
|
||||
* The auth text field — a {@link ChipInput} raised to the auth control height
|
||||
* ({@link AUTH_CONTROL_HEIGHT}) so every labeled field on the auth and invite
|
||||
* surfaces shares one slightly-taller geometry. All chip props pass through
|
||||
* (`error`, `endAdornment`, `icon`, …); only the height is owned here, and a
|
||||
* caller's `className` (layout only) still composes on top.
|
||||
*/
|
||||
export const AuthInput = React.forwardRef<HTMLInputElement, ChipInputProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ChipInput ref={ref} className={cn(AUTH_CONTROL_HEIGHT, className)} {...props} />
|
||||
)
|
||||
)
|
||||
|
||||
AuthInput.displayName = 'AuthInput'
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AuthTextLink } from '@/app/(auth)/components/auth-text-link'
|
||||
|
||||
interface AuthLegalFooterProps {
|
||||
/** The gerund describing the consent action, e.g. "signing in". */
|
||||
action: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The "By {action}, you agree to our Terms / Privacy" fine print shared by the
|
||||
* login and signup pages. Restyled to muted light tokens with the legal links
|
||||
* routed through {@link AuthTextLink}, so the consent copy has one source.
|
||||
*/
|
||||
export function AuthLegalFooter({ action }: AuthLegalFooterProps) {
|
||||
return (
|
||||
<p className='text-center text-[var(--text-muted)] text-caption leading-relaxed'>
|
||||
By {action}, you agree to our{' '}
|
||||
<AuthTextLink href='/terms' external>
|
||||
Terms of Service
|
||||
</AuthTextLink>{' '}
|
||||
and{' '}
|
||||
<AuthTextLink href='/privacy' external>
|
||||
Privacy Policy
|
||||
</AuthTextLink>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ChipLink } from '@sim/emcn'
|
||||
|
||||
interface AuthNavPromptProps {
|
||||
/** Muted lead text before the link (e.g. "Don't have an account?"). */
|
||||
prompt?: string
|
||||
href: string
|
||||
linkLabel: string
|
||||
/** Side effect to run before navigation (e.g. clearing verification state). */
|
||||
onNavigate?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The cross-page navigation row (Sign up / Sign in / Back to login) — an
|
||||
* optional muted prompt followed by an outline {@link ChipLink} pill, matching
|
||||
* the landing's secondary chip CTAs. Centralizes the auth nav affordance so the
|
||||
* pill chrome is described by props, never restyled per page.
|
||||
*/
|
||||
export function AuthNavPrompt({ prompt, href, linkLabel, onNavigate }: AuthNavPromptProps) {
|
||||
return (
|
||||
<div className='flex items-center justify-center gap-1 text-sm'>
|
||||
{prompt && <span className='text-[var(--text-muted)]'>{prompt}</span>}
|
||||
<ChipLink href={href} onClick={onNavigate} className='border border-[var(--border-1)]'>
|
||||
{linkLabel}
|
||||
</ChipLink>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { LogoMark, SimWordmark } from '@/app/(landing)/components/navbar/components'
|
||||
|
||||
interface AuthShellProps {
|
||||
/** Centered content column (the form, status copy, etc.). */
|
||||
children: ReactNode
|
||||
/** Optional element pinned to the bottom of the shell (e.g. the support footer). */
|
||||
footer?: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* The light auth/status page frame — the single source of truth for the shell
|
||||
* every auth page and standalone status page wears.
|
||||
*
|
||||
* Mirrors the landing chrome: it pins the `light` token layer (so the platform's
|
||||
* light-mode `var(--*)` tokens resolve regardless of the visitor's theme), uses
|
||||
* the canvas/`--text-primary` surface, and renders a logo-only header that reuses
|
||||
* the landing {@link LogoMark} + {@link SimWordmark} at the same nav gutters. The
|
||||
* single content column is centered and capped for a calm single-form layout.
|
||||
*/
|
||||
export function AuthShell({ children, footer }: AuthShellProps) {
|
||||
return (
|
||||
<div className='light relative flex min-h-screen flex-col bg-[var(--bg)] text-[var(--text-primary)]'>
|
||||
<header>
|
||||
<nav className='mx-auto flex w-full max-w-[1446px] items-center px-12 py-4 max-sm:px-5 max-lg:px-8'>
|
||||
<Link href='/' aria-label='Sim home' className='flex h-[30px] items-center'>
|
||||
<LogoMark>
|
||||
<SimWordmark />
|
||||
</LogoMark>
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
<div className='flex flex-1 items-center justify-center px-4 pb-16'>
|
||||
<div className='w-full max-w-[400px]'>{children}</div>
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Chip, Loader } from '@sim/emcn'
|
||||
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
|
||||
|
||||
interface AuthSubmitButtonProps {
|
||||
children: ReactNode
|
||||
/** Label shown beside the spinner while the action is in flight. */
|
||||
loadingLabel: string
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
type?: 'submit' | 'button'
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical full-width primary auth action — a `primary`-variant {@link Chip}
|
||||
* with the shared in-flight spinner, so the primary CTA chrome lives in
|
||||
* exactly one place.
|
||||
*/
|
||||
export function AuthSubmitButton({
|
||||
children,
|
||||
loadingLabel,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
type = 'submit',
|
||||
onClick,
|
||||
}: AuthSubmitButtonProps) {
|
||||
return (
|
||||
<Chip
|
||||
variant='primary'
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
disabled={disabled || loading}
|
||||
fullWidth
|
||||
flush
|
||||
className={AUTH_BUTTON_CLASS}
|
||||
>
|
||||
{loading ? (
|
||||
<span className='flex items-center gap-2'>
|
||||
<Loader className='size-4' animate />
|
||||
{loadingLabel}
|
||||
</span>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@sim/emcn'
|
||||
import Link from 'next/link'
|
||||
|
||||
const AUTH_TEXT_LINK_CLASS =
|
||||
'text-[var(--text-secondary)] underline-offset-4 transition-colors hover:text-[var(--text-primary)] hover:underline disabled:cursor-not-allowed disabled:opacity-50'
|
||||
|
||||
interface AuthTextLinkProps {
|
||||
children: ReactNode
|
||||
/** Renders a navigation link when set; otherwise renders an action button. */
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
/** Opens the link in a new tab with safe `rel` (e.g. Terms/Privacy). */
|
||||
external?: boolean
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical inline text affordance for the auth pages — forgot-password,
|
||||
* resend, and the legal links. Renders a {@link Link} when `href` is set and a
|
||||
* `<button>` otherwise, both in one light-token style. Replaces the legacy dark
|
||||
* `AUTH_TEXT_LINK` class string with a single props-driven source of truth.
|
||||
*/
|
||||
export function AuthTextLink({
|
||||
children,
|
||||
href,
|
||||
onClick,
|
||||
external = false,
|
||||
disabled = false,
|
||||
className,
|
||||
}: AuthTextLinkProps) {
|
||||
if (href) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
onClick={onClick}
|
||||
className={cn(AUTH_TEXT_LINK_CLASS, className)}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cn(AUTH_TEXT_LINK_CLASS, className)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Auth and invite surfaces use a slightly taller control than the 30px chip
|
||||
* default, matching the landing `HeroCta` field family (the landing's own
|
||||
* auth-adjacent CTA renders taller fields than in-app chips). Applied as the
|
||||
* single source of truth for every auth field and button height so the inputs,
|
||||
* submit, social, SSO, and invite action buttons stay on one line.
|
||||
*/
|
||||
export const AUTH_CONTROL_HEIGHT = 'h-9'
|
||||
|
||||
/**
|
||||
* Shared layout for full-width auth/invite chip buttons (submit, social, SSO,
|
||||
* invite actions). `[&>span]:flex-none` collapses the chip's stretching label
|
||||
* span — which carries `flex-1` — so the icon + label cluster truly centers
|
||||
* under `justify-center` (the landing `HeroCta` idiom). Height-only inputs use
|
||||
* {@link AUTH_CONTROL_HEIGHT}; buttons compose this on top of it.
|
||||
*/
|
||||
export const AUTH_BUTTON_CLASS = `${AUTH_CONTROL_HEIGHT} w-full justify-center [&>span]:flex-none`
|
||||
@@ -0,0 +1,14 @@
|
||||
export { AuthDivider } from './auth-divider'
|
||||
export { AuthField } from './auth-field'
|
||||
export { AuthFormMessage } from './auth-form-message'
|
||||
export { AuthHeader } from './auth-header'
|
||||
export { AuthInput } from './auth-input'
|
||||
export { AuthLegalFooter } from './auth-legal-footer'
|
||||
export { AuthNavPrompt } from './auth-nav-prompt'
|
||||
export { AuthShell } from './auth-shell'
|
||||
export { AuthSubmitButton } from './auth-submit-button'
|
||||
export { AuthTextLink } from './auth-text-link'
|
||||
export { PasswordInput } from './password-input'
|
||||
export { SocialLoginButtons } from './social-login-buttons'
|
||||
export { SSOLoginButton } from './sso-login-button'
|
||||
export { SupportFooter } from './support-footer'
|
||||
@@ -0,0 +1,20 @@
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import {
|
||||
isGithubAuthDisabled,
|
||||
isGoogleAuthDisabled,
|
||||
isMicrosoftAuthDisabled,
|
||||
isProd,
|
||||
} from '@/lib/core/config/env-flags'
|
||||
|
||||
export async function getOAuthProviderStatus() {
|
||||
const githubAvailable =
|
||||
!!(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET) && !isGithubAuthDisabled
|
||||
|
||||
const googleAvailable =
|
||||
!!(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) && !isGoogleAuthDisabled
|
||||
|
||||
const microsoftAvailable =
|
||||
!!(env.MICROSOFT_CLIENT_ID && env.MICROSOFT_CLIENT_SECRET) && !isMicrosoftAuthDisabled
|
||||
|
||||
return { githubAvailable, googleAvailable, microsoftAvailable, isProduction: isProd }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ChipInput, type ChipInputProps, cn } from '@sim/emcn'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import { AUTH_CONTROL_HEIGHT } from '@/app/(auth)/components/constants'
|
||||
|
||||
type PasswordInputProps = Omit<ChipInputProps, 'type' | 'icon' | 'endAdornment'>
|
||||
|
||||
/**
|
||||
* A {@link ChipInput} that owns the password reveal toggle — the eye button is
|
||||
* driven through the canonical `endAdornment` slot and the field's invalid state
|
||||
* through the `error` prop, so no consumer hand-rolls the relative wrapper +
|
||||
* absolutely positioned button the auth forms previously duplicated four times.
|
||||
*/
|
||||
export function PasswordInput({ error, className, ...props }: PasswordInputProps) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
return (
|
||||
<ChipInput
|
||||
{...props}
|
||||
className={cn(AUTH_CONTROL_HEIGHT, className)}
|
||||
type={visible ? 'text' : 'password'}
|
||||
error={error}
|
||||
endAdornment={
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setVisible((v) => !v)}
|
||||
aria-label={visible ? 'Hide password' : 'Show password'}
|
||||
className='flex shrink-0 text-[var(--text-icon)] transition-colors hover:text-[var(--text-primary)]'
|
||||
>
|
||||
{visible ? <EyeOff className='size-[14px]' /> : <Eye className='size-[14px]' />}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, useState } from 'react'
|
||||
import { Chip, cn } from '@sim/emcn'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { GithubIcon, GoogleIcon, MicrosoftIcon } from '@/components/icons'
|
||||
import { client } from '@/lib/auth/auth-client'
|
||||
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
|
||||
|
||||
const logger = createLogger('SocialLoginButtons')
|
||||
|
||||
interface SocialLoginButtonsProps {
|
||||
githubAvailable: boolean
|
||||
googleAvailable: boolean
|
||||
microsoftAvailable: boolean
|
||||
callbackURL?: string
|
||||
isProduction: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export function SocialLoginButtons({
|
||||
githubAvailable,
|
||||
googleAvailable,
|
||||
microsoftAvailable,
|
||||
callbackURL = '/workspace',
|
||||
isProduction,
|
||||
children,
|
||||
}: SocialLoginButtonsProps) {
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false)
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false)
|
||||
const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false)
|
||||
|
||||
async function signInWithGithub() {
|
||||
if (!githubAvailable) return
|
||||
|
||||
setIsGithubLoading(true)
|
||||
try {
|
||||
await client.signIn.social({ provider: 'github', callbackURL })
|
||||
} catch (err) {
|
||||
logger.error('GitHub sign-in failed', { error: getErrorMessage(err) })
|
||||
} finally {
|
||||
setIsGithubLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function signInWithGoogle() {
|
||||
if (!googleAvailable) return
|
||||
|
||||
setIsGoogleLoading(true)
|
||||
try {
|
||||
await client.signIn.social({ provider: 'google', callbackURL })
|
||||
} catch (err) {
|
||||
logger.error('Google sign-in failed', { error: getErrorMessage(err) })
|
||||
} finally {
|
||||
setIsGoogleLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function signInWithMicrosoft() {
|
||||
if (!microsoftAvailable) return
|
||||
|
||||
setIsMicrosoftLoading(true)
|
||||
try {
|
||||
await client.signIn.social({ provider: 'microsoft', callbackURL })
|
||||
} catch (err) {
|
||||
logger.error('Microsoft sign-in failed', { error: getErrorMessage(err) })
|
||||
} finally {
|
||||
setIsMicrosoftLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const githubButton = (
|
||||
<Chip
|
||||
fullWidth
|
||||
flush
|
||||
leftIcon={GithubIcon}
|
||||
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
|
||||
disabled={!githubAvailable || isGithubLoading}
|
||||
onClick={signInWithGithub}
|
||||
>
|
||||
{isGithubLoading ? 'Connecting…' : 'GitHub'}
|
||||
</Chip>
|
||||
)
|
||||
|
||||
const googleButton = (
|
||||
<Chip
|
||||
fullWidth
|
||||
flush
|
||||
leftIcon={GoogleIcon}
|
||||
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
|
||||
disabled={!googleAvailable || isGoogleLoading}
|
||||
onClick={signInWithGoogle}
|
||||
>
|
||||
{isGoogleLoading ? 'Connecting…' : 'Google'}
|
||||
</Chip>
|
||||
)
|
||||
|
||||
const microsoftButton = (
|
||||
<Chip
|
||||
fullWidth
|
||||
flush
|
||||
leftIcon={MicrosoftIcon}
|
||||
className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')}
|
||||
disabled={!microsoftAvailable || isMicrosoftLoading}
|
||||
onClick={signInWithMicrosoft}
|
||||
>
|
||||
{isMicrosoftLoading ? 'Connecting…' : 'Microsoft'}
|
||||
</Chip>
|
||||
)
|
||||
|
||||
const hasAnyOAuthProvider = githubAvailable || googleAvailable || microsoftAvailable
|
||||
|
||||
if (!hasAnyOAuthProvider && !children) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid gap-3'>
|
||||
{googleAvailable && googleButton}
|
||||
{microsoftAvailable && microsoftButton}
|
||||
{githubAvailable && githubButton}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
import { Chip, cn } from '@sim/emcn'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants'
|
||||
|
||||
interface SSOLoginButtonProps {
|
||||
callbackURL?: string
|
||||
className?: string
|
||||
variant?: 'primary' | 'outline'
|
||||
}
|
||||
|
||||
export function SSOLoginButton({
|
||||
callbackURL,
|
||||
className,
|
||||
variant = 'outline',
|
||||
}: SSOLoginButtonProps) {
|
||||
const router = useRouter()
|
||||
|
||||
if (!isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED'))) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSSOClick = () => {
|
||||
const ssoUrl = `/sso${callbackURL ? `?callbackUrl=${encodeURIComponent(callbackURL)}` : ''}`
|
||||
router.push(ssoUrl)
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
variant={variant === 'primary' ? 'primary' : undefined}
|
||||
fullWidth
|
||||
flush
|
||||
onClick={handleSSOClick}
|
||||
className={cn(
|
||||
AUTH_BUTTON_CLASS,
|
||||
variant === 'outline' && 'border border-[var(--border-1)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
Sign in with SSO
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@sim/emcn'
|
||||
import { useBrandConfig } from '@/ee/whitelabeling'
|
||||
|
||||
export interface SupportFooterProps {
|
||||
/**
|
||||
* `fixed`/`absolute` pin the footer over the page (short, centered forms
|
||||
* only — content must never render underneath it). `static` renders it in
|
||||
* normal document flow after the content, which is required for pages with
|
||||
* unbounded content height (e.g. the resume gate's HITL form): an
|
||||
* absolutely-positioned footer with no reserved space is not pushed down by
|
||||
* flow content, so it silently overlaps and eats clicks on whatever content
|
||||
* ends up in its footprint.
|
||||
*/
|
||||
position?: 'fixed' | 'absolute' | 'static'
|
||||
}
|
||||
|
||||
export function SupportFooter({ position = 'fixed' }: SupportFooterProps) {
|
||||
const brandConfig = useBrandConfig()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'pb-8 text-center text-[var(--text-muted)] text-caption leading-relaxed',
|
||||
position !== 'static' && 'right-0 bottom-0 left-0 z-50',
|
||||
position
|
||||
)}
|
||||
>
|
||||
Need help?{' '}
|
||||
<a
|
||||
href={`mailto:${brandConfig.supportEmail}`}
|
||||
className='text-[var(--text-muted)] underline-offset-4 transition-colors hover:text-[var(--text-body)] hover:underline'
|
||||
>
|
||||
Contact support
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user