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