diff --git a/app/components/auth-page.tsx b/app/components/auth-page.tsx deleted file mode 100644 index 191d8724..00000000 --- a/app/components/auth-page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { type ReactNode } from 'react' - -export function AuthPage({ - title, - description, - children, - footer, -}: { - title: string - description?: ReactNode - children: ReactNode - footer?: ReactNode -}) { - return ( -
-
-
-

- GratiText -

-

- {title} -

- {description ? ( -

- {description} -

- ) : null} -
-
- {children} -
- {footer ? ( -
- {footer} -
- ) : null} -
-
- ) -} diff --git a/app/components/dialog-page.tsx b/app/components/dialog-page.tsx new file mode 100644 index 00000000..d8017a77 --- /dev/null +++ b/app/components/dialog-page.tsx @@ -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(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 ( +
+ +
+ +
+
+ ) +} diff --git a/app/components/form-page.tsx b/app/components/form-page.tsx new file mode 100644 index 00000000..8ec65bbb --- /dev/null +++ b/app/components/form-page.tsx @@ -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 ( + +
+
+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+
+ {children} +
+ {footer ? ( +
+ {footer} +
+ ) : null} +
+
+ ) +} + +/** + * 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 ( +
+
+ {aside} +
+
+ {children} +
+
+ ) +} diff --git a/app/components/forms.tsx b/app/components/forms.tsx index f9995f03..ea5235ba 100644 --- a/app/components/forms.tsx +++ b/app/components/forms.tsx @@ -4,7 +4,7 @@ import { REGEXP_ONLY_DIGITS, type OTPInputProps, } from 'input-otp' -import React, { useId } from 'react' +import React, { useId, useState } from 'react' import { cn } from '#app/utils/misc.tsx' import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx' import { Icon } from './ui/icon.tsx' @@ -32,7 +32,7 @@ export function ErrorList({ return (