From 0efa8a30bc0095bb44490e962f33244fb4b96ba5 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 11:46:51 -0700 Subject: [PATCH 1/2] refactor(auth): share loading skeleton treatments --- .../components/auth-loading-skeleton.tsx | 58 +++++++++++++++++++ apps/sim/app/(auth)/login/loading.tsx | 33 +++++------ .../sim/app/(auth)/reset-password/loading.tsx | 23 ++++---- apps/sim/app/(auth)/signup/loading.tsx | 38 +++++------- apps/sim/app/(auth)/sso/loading.tsx | 23 ++++---- apps/sim/app/(auth)/verify/loading.tsx | 17 +++--- 6 files changed, 121 insertions(+), 71 deletions(-) create mode 100644 apps/sim/app/(auth)/components/auth-loading-skeleton.tsx diff --git a/apps/sim/app/(auth)/components/auth-loading-skeleton.tsx b/apps/sim/app/(auth)/components/auth-loading-skeleton.tsx new file mode 100644 index 00000000000..be5968cec46 --- /dev/null +++ b/apps/sim/app/(auth)/components/auth-loading-skeleton.tsx @@ -0,0 +1,58 @@ +import type { ReactNode } from 'react' +import { cn, Skeleton } from '@sim/emcn' +import { cva, type VariantProps } from 'class-variance-authority' + +const authLoadingSkeletonVariants = cva('', { + variants: { + shape: { + title: 'h-[38px] rounded-[4px]', + label: 'h-[14px] rounded-[4px]', + control: 'h-[44px] rounded-[10px]', + divider: 'h-[1px] rounded-[1px]', + }, + }, +}) + +interface AuthLoadingSkeletonProps extends VariantProps { + className?: string +} + +/** Placeholder geometry shared by the auth route loading states. */ +export function AuthLoadingSkeleton({ shape, className }: AuthLoadingSkeletonProps) { + return +} + +interface AuthLoadingFrameProps { + children: ReactNode +} + +export function AuthLoadingFrame({ children }: AuthLoadingFrameProps) { + return
{children}
+} + +interface AuthLoadingFieldProps { + labelWidthClassName: string + className?: string +} + +export function AuthLoadingField({ labelWidthClassName, className }: AuthLoadingFieldProps) { + return ( +
+ + +
+ ) +} + +/** The divider and paired provider-action placeholders on login and signup. */ +export function AuthLoadingAlternateActions() { + return ( + <> + +
+ + +
+ + ) +} diff --git a/apps/sim/app/(auth)/login/loading.tsx b/apps/sim/app/(auth)/login/loading.tsx index 74f7bece6c9..5ed074044be 100644 --- a/apps/sim/app/(auth)/login/loading.tsx +++ b/apps/sim/app/(auth)/login/loading.tsx @@ -1,24 +1,19 @@ -import { Skeleton } from '@sim/emcn' +import { + AuthLoadingAlternateActions, + AuthLoadingField, + AuthLoadingFrame, + AuthLoadingSkeleton, +} from '@/app/(auth)/components/auth-loading-skeleton' export default function LoginLoading() { return ( -
- -
- - -
-
- - -
- - -
- - -
- -
+ + + + + + + + ) } diff --git a/apps/sim/app/(auth)/reset-password/loading.tsx b/apps/sim/app/(auth)/reset-password/loading.tsx index d1910ac0425..6535a7e6e86 100644 --- a/apps/sim/app/(auth)/reset-password/loading.tsx +++ b/apps/sim/app/(auth)/reset-password/loading.tsx @@ -1,16 +1,17 @@ -import { Skeleton } from '@sim/emcn' +import { + AuthLoadingField, + AuthLoadingFrame, + AuthLoadingSkeleton, +} from '@/app/(auth)/components/auth-loading-skeleton' export default function ResetPasswordLoading() { return ( -
- - -
- - -
- - -
+ + + + + + + ) } diff --git a/apps/sim/app/(auth)/signup/loading.tsx b/apps/sim/app/(auth)/signup/loading.tsx index c1190c8385e..bb16f3cd39d 100644 --- a/apps/sim/app/(auth)/signup/loading.tsx +++ b/apps/sim/app/(auth)/signup/loading.tsx @@ -1,28 +1,20 @@ -import { Skeleton } from '@sim/emcn' +import { + AuthLoadingAlternateActions, + AuthLoadingField, + AuthLoadingFrame, + AuthLoadingSkeleton, +} from '@/app/(auth)/components/auth-loading-skeleton' export default function SignupLoading() { return ( -
- -
- - -
-
- - -
-
- - -
- - -
- - -
- -
+ + + + + + + + + ) } diff --git a/apps/sim/app/(auth)/sso/loading.tsx b/apps/sim/app/(auth)/sso/loading.tsx index 116e47c136b..fc36d8d1835 100644 --- a/apps/sim/app/(auth)/sso/loading.tsx +++ b/apps/sim/app/(auth)/sso/loading.tsx @@ -1,16 +1,17 @@ -import { Skeleton } from '@sim/emcn' +import { + AuthLoadingField, + AuthLoadingFrame, + AuthLoadingSkeleton, +} from '@/app/(auth)/components/auth-loading-skeleton' export default function SSOLoading() { return ( -
- - -
- - -
- - -
+ + + + + + + ) } diff --git a/apps/sim/app/(auth)/verify/loading.tsx b/apps/sim/app/(auth)/verify/loading.tsx index d884048905a..4fa87a42400 100644 --- a/apps/sim/app/(auth)/verify/loading.tsx +++ b/apps/sim/app/(auth)/verify/loading.tsx @@ -1,12 +1,15 @@ -import { Skeleton } from '@sim/emcn' +import { + AuthLoadingFrame, + AuthLoadingSkeleton, +} from '@/app/(auth)/components/auth-loading-skeleton' export default function VerifyLoading() { return ( -
- - - - -
+ + + + + + ) } From a8250129539f9acba1475df08e4e0780d8333991 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Thu, 24 Sep 2026 12:29:19 -0700 Subject: [PATCH 2/2] test(auth): cover shared loading shapes and composition --- .../components/auth-loading-skeleton.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx diff --git a/apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx b/apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx new file mode 100644 index 00000000000..0fff2b496f4 --- /dev/null +++ b/apps/sim/app/(auth)/components/auth-loading-skeleton.test.tsx @@ -0,0 +1,54 @@ +/** + * @vitest-environment jsdom + */ +import type { ReactElement } from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it } from 'vitest' +import { + AuthLoadingAlternateActions, + AuthLoadingField, + AuthLoadingFrame, + AuthLoadingSkeleton, +} from '@/app/(auth)/components/auth-loading-skeleton' + +function render(element: ReactElement): HTMLDivElement { + const container = document.createElement('div') + container.innerHTML = renderToStaticMarkup(element) + return container +} + +describe('auth loading skeletons', () => { + it.each([ + ['title', 'h-[38px]', 'rounded-[4px]'], + ['label', 'h-[14px]', 'rounded-[4px]'], + ['control', 'h-[44px]', 'rounded-[10px]'], + ['divider', 'h-[1px]', 'rounded-[1px]'], + ] as const)('retains the %s placeholder geometry', (shape, height, radius) => { + const container = render() + const skeleton = container.firstElementChild + + expect(skeleton?.classList.contains('animate-pulse')).toBe(true) + expect(skeleton?.classList.contains('bg-[var(--surface-active)]')).toBe(true) + expect(skeleton?.classList.contains(height)).toBe(true) + expect(skeleton?.classList.contains(radius)).toBe(true) + expect(skeleton?.classList.contains('w-[93px]')).toBe(true) + }) + + it('keeps one label, three controls, and one divider in the shared field and actions', () => { + const container = render( + + + + + ) + const frame = container.firstElementChild + const skeletons = Array.from(container.querySelectorAll('.animate-pulse')) + + expect(frame?.classList.contains('flex-col')).toBe(true) + expect(skeletons).toHaveLength(5) + expect(skeletons.filter((el) => el.classList.contains('h-[14px]'))).toHaveLength(1) + expect(skeletons.filter((el) => el.classList.contains('h-[44px]'))).toHaveLength(3) + expect(skeletons.filter((el) => el.classList.contains('h-[1px]'))).toHaveLength(1) + expect(skeletons[0].classList.contains('w-[86px]')).toBe(true) + }) +})