Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 0 additions & 41 deletions app/components/auth-page.tsx

This file was deleted.

93 changes: 93 additions & 0 deletions app/components/dialog-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { type ReactNode, useCallback, useRef } from 'react'
import { Link, useNavigate } from 'react-router'
import { useFocusTrap } from '#app/utils/focus-trap.ts'
import { useMediaQuery } from '#app/utils/media-query.ts'
import { cn } from '#app/utils/misc.tsx'
import { Icon } from './ui/icon.tsx'

/**
* A route rendered as a modal dialog over its parent page on desktop and as a
* plain full-width page (with a "go back" link) on phones — the pattern the
* account settings sub-pages use in the designs.
*/
export function DialogPage({
title,
description,
backTo,
backLabel = 'Go back without changes',
hideClose = false,
children,
className,
}: {
title: string
description?: ReactNode
backTo: string
backLabel?: string
/** Skip the corner close button when the content has its own Cancel. */
hideClose?: boolean
children: ReactNode
className?: string
}) {
const navigate = useNavigate()
const panelRef = useRef<HTMLDivElement>(null)
const close = useCallback(() => {
void navigate(backTo)
}, [navigate, backTo])
// The panel is only a modal when it floats over the page (md and up); on
// phones it is an ordinary page and the header stays usable.
const isDesktop = useMediaQuery('(min-width: 768px)')
useFocusTrap(panelRef, isDesktop, close)

return (
<div className="flex flex-1 flex-col md:fixed md:inset-0 md:z-50 md:flex-none md:items-center md:justify-center md:p-6">
<Link
to={backTo}
tabIndex={-1}
aria-hidden="true"
className="bg-overlay/70 absolute inset-0 hidden backdrop-blur-[4px] md:block"
/>
<div
ref={panelRef}
role={isDesktop ? 'dialog' : undefined}
aria-modal={isDesktop ? 'true' : undefined}
aria-labelledby="dialog-page-title"
Comment thread
cursor[bot] marked this conversation as resolved.
tabIndex={-1}
className={cn(
'md:bg-card md:text-card-foreground container flex flex-1 flex-col pt-6 pb-6 outline-none md:relative md:max-h-full md:w-full md:max-w-[46.5rem] md:flex-none md:overflow-y-auto md:rounded-[1.75rem] md:px-12 md:py-11 md:shadow-[0_32px_64px_-24px_rgba(24,36,48,0.35)]',
className,
)}
>
<Link
to={backTo}
className="text-foreground hover:text-brand -ml-1 inline-flex w-fit items-center gap-2 text-base transition-colors md:hidden"
>
<Icon name="chevron-left" size="sm" aria-hidden="true" />
{backLabel}
</Link>
{hideClose ? null : (
<Link
to={backTo}
aria-label="Close"
className="text-muted-foreground hover:bg-muted hover:text-foreground absolute top-6 right-6 hidden h-10 w-10 items-center justify-center rounded-full transition-colors md:flex"
>
<Icon name="cross-1" size="sm" aria-hidden="true" />
</Link>
)}
<h1
id="dialog-page-title"
className="font-display text-foreground md:text-h3 sr-only md:not-sr-only"
>
{title}
</h1>
{description ? (
<p className="text-muted-foreground hidden text-base md:mt-4 md:block">
{description}
</p>
) : null}
<div className="mt-6 flex flex-1 flex-col md:mt-8 md:flex-none">
{children}
</div>
</div>
</div>
)
}
84 changes: 84 additions & 0 deletions app/components/form-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { type ReactNode } from 'react'
import { cn } from '#app/utils/misc.tsx'

/**
* Auth routes export this handle so the app layout tints the header to match
* the full-bleed beige auth page on small screens and drops the sign-up /
* log-in buttons that would compete with the form.
*/
export const authPageHandle = { pageTint: 'hero', chrome: 'minimal' } as const

/** Dashboard forms keep the normal header but share the auth page shell. */
export const formPageHandle = { pageTint: 'hero' } as const

export function FormPage({
title,
description,
children,
footer,
as = 'main',
}: {
title: string
description?: ReactNode
children: ReactNode
footer?: ReactNode
/** Use `div` when an ancestor route already renders the `main` landmark. */
as?: 'main' | 'div'
}) {
const Wrapper = as
return (
<Wrapper className="bg-hero md:bg-background flex flex-1 flex-col pt-10 pb-6 md:pt-20 md:pb-24">
<div className="container flex flex-1 flex-col md:max-w-[52rem] md:flex-none">
<div className="text-center">
<h1 className="font-display text-foreground md:text-h2 text-[1.875rem] leading-[1.15] text-balance">
{title}
</h1>
{description ? (
<p className="text-muted-foreground mx-auto mt-4 max-w-xl text-base text-pretty md:mt-5">
{description}
</p>
) : null}
</div>
<div className="md:bg-surface md:text-surface-foreground mt-10 flex flex-1 flex-col md:mt-14 md:flex-none md:rounded-[1.5rem] md:px-9 md:py-9 lg:px-9">
{children}
</div>
{footer ? (
<div className="text-muted-foreground [&_a]:text-foreground mt-6 text-center text-sm md:mt-8 [&_a]:inline-flex [&_a]:items-center [&_a]:gap-2 [&_a]:font-medium [&_a]:underline-offset-4 hover:[&_a]:underline">
{footer}
</div>
) : null}
</div>
</Wrapper>
)
}

/**
* Bottom row of an auth form: optional helper text on the left and the
* primary action on the right. On phones the action becomes a full-width
* button anchored to the bottom of the screen, as in the mobile designs.
*/
export function FormActions({
aside,
children,
className,
}: {
aside?: ReactNode
children: ReactNode
className?: string
}) {
return (
<div
className={cn(
'mt-auto flex flex-col gap-5 pt-6 md:mt-0 md:flex-row md:items-center md:justify-between md:gap-6 md:pt-2',
className,
)}
>
<div className="text-foreground [&_a]:text-foreground text-sm [&_a]:underline-offset-4 hover:[&_a]:underline">
{aside}
</div>
<div className="flex flex-col gap-3 md:flex-row md:items-center [&>*]:w-full md:[&>*]:w-auto">
{children}
</div>
</div>
)
}
Loading
Loading