From 0440fb2b893e77f5345bb1d6b91457b658b1a7d3 Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Thu, 2 Jul 2026 17:18:40 -0700 Subject: [PATCH] feat(dashboard): add interactive custom tabs to hosted auth preview Replace Radix tabs with lightweight preview tabs and remove tabIndex blocking so inputs, links, and tab switches work in the live preview. Co-authored-by: Cursor --- .../src/components/hosted-auth-preview.tsx | 182 +++++++++++++++--- 1 file changed, 155 insertions(+), 27 deletions(-) diff --git a/apps/dashboard/src/components/hosted-auth-preview.tsx b/apps/dashboard/src/components/hosted-auth-preview.tsx index 8931c2e3b..7ed946c55 100644 --- a/apps/dashboard/src/components/hosted-auth-preview.tsx +++ b/apps/dashboard/src/components/hosted-auth-preview.tsx @@ -1,8 +1,8 @@ "use client"; -import { BrandIcons, Button, Input, Label, Separator, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui"; +import { BrandIcons, Button, Input, Label, Separator, Typography, cn } from "@/components/ui"; import { KeyIcon } from "@phosphor-icons/react"; -import type { ReactElement, ReactNode } from "react"; +import React, { useCallback, useLayoutEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactElement, type ReactNode } from "react"; type HostedPreviewOAuthProvider = { id: string, @@ -25,6 +25,137 @@ const authTabsListClassName = "mb-4 h-10 w-full rounded-lg border border-black/[ const authTabsTriggerClassName = "h-8 flex-1 rounded-md py-0 text-sm font-medium text-muted-foreground transition-colors duration-300 hover:text-foreground/90 data-[state=active]:font-semibold data-[state=active]:text-foreground"; const authFooterClassName = "mt-6 border-t border-black/[0.06] pt-5 text-center text-sm dark:border-white/[0.10]"; const authFooterLinkClassName = "font-medium text-foreground/90 underline-offset-4 transition-colors hover:text-foreground hover:underline"; +const authInputClassName = "h-9 rounded-lg border-black/[0.08] bg-white/45 px-3 py-1 text-sm shadow-none ring-0 placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring dark:border-white/[0.15] dark:bg-zinc-900/50"; + +type HostedPreviewTabsContextValue = { + value: string, + setValue: (value: string) => void, +}; + +const HostedPreviewTabsContext = React.createContext(null); + +function useHostedPreviewTabsContext() { + const context = React.useContext(HostedPreviewTabsContext); + if (context == null) { + throw new Error("Hosted preview tabs components must be rendered inside HostedPreviewTabs"); + } + return context; +} + +function HostedPreviewTabs(props: HTMLAttributes & { + defaultValue: string, +}) { + const [value, setValue] = useState(props.defaultValue); + const contextValue = useMemo(() => ({ + value, + setValue, + }), [value]); + + return ( + +
+ + ); +} + +function HostedPreviewTabsList({ className, children, ...props }: HTMLAttributes) { + const tabs = useHostedPreviewTabsContext(); + const containerRef = useRef(null); + const [indicatorStyle, setIndicatorStyle] = useState<{ left: number, top: number, width: number, height: number } | null>(null); + + const measure = useCallback(() => { + const container = containerRef.current; + if (container == null) { + return; + } + const activeTab = container.querySelector('[role="tab"][data-state="active"]'); + if (activeTab == null) { + setIndicatorStyle(null); + return; + } + setIndicatorStyle({ + left: activeTab.offsetLeft, + top: activeTab.offsetTop, + width: activeTab.offsetWidth, + height: activeTab.offsetHeight, + }); + }, []); + + useLayoutEffect(() => { + measure(); + }, [measure, tabs.value]); + + useLayoutEffect(() => { + const container = containerRef.current; + if (container == null || typeof ResizeObserver === "undefined") { + return; + } + const observer = new ResizeObserver(() => measure()); + observer.observe(container); + return () => observer.disconnect(); + }, [measure]); + + return ( +
+ {indicatorStyle != null && ( +
+ )} + {children} +
+ ); +} + +function HostedPreviewTabsTrigger({ className, value, onClick, ...props }: React.ButtonHTMLAttributes & { + value: string, +}) { + const tabs = useHostedPreviewTabsContext(); + const active = tabs.value === value; + + return ( + @@ -213,20 +343,19 @@ function HostedCredentialPreviewForm(props: { type: HostedAuthType, }) { return ( -
+ event.preventDefault()}>
{props.type === "sign-in" && ( - + event.preventDefault()}> Forgot password? )} @@ -235,11 +364,10 @@ function HostedCredentialPreviewForm(props: { id="hosted-preview-email-password-password" type="password" autoComplete={props.type === "sign-in" ? "current-password" : "new-password"} - className="h-10 rounded-xl border-border bg-background" - tabIndex={-1} + className={authInputClassName} /> - @@ -291,20 +419,20 @@ export function HostedAuthMethodPreview(props: { {enableSeparator && } {props.project.config.credentialEnabled && props.project.config.magicLinkEnabled ? ( - - + - Email - Email & Password - - + Email + Email & Password + + - - + + - - + + ) : props.project.config.credentialEnabled ? ( ) : props.project.config.magicLinkEnabled ? ( @@ -318,14 +446,14 @@ export function HostedAuthMethodPreview(props: { {type === "sign-in" ? (

Don't have an account?{" "} - + event.preventDefault()}> Sign up

) : (

Already have an account?{" "} - + event.preventDefault()}> Sign in