Refactor profile layout and navigation: align BackButton with form padding, simplify ProfileFormSectionBar behavior, adjust spacing for consistent visual hierarchy.

This commit is contained in:
MartinBraquet
2026-07-28 19:39:04 +02:00
parent 766de00d7c
commit c7150da21f
5 changed files with 23 additions and 28 deletions

View File

@@ -22,7 +22,10 @@ export function BackButton(props: {className?: string}) {
type="button"
onClick={() => router.back()}
className={clsx(
'text-ink-500 hover:text-primary-700 inline-flex items-center gap-1 text-sm transition-all',
// `-m-2 p-2` is hit area only: on mobile the label is hidden and the button was the 16px
// chevron itself, well under a thumb's worth of target. The negative margin cancels the
// padding, so the chevron stays exactly where every caller already positions it.
'text-ink-500 hover:text-primary-700 -m-2 inline-flex items-center gap-1 p-2 text-sm transition-all',
className,
)}
>

View File

@@ -134,31 +134,17 @@ export function ProfileFormNav(props: {className?: string}) {
* length. The need is greater here than on desktop, since every field stacks and the page runs
* roughly three times longer; the answer just has to cost one row instead of a screen.
*
* It hides while you scroll down and returns when you scroll up. A permanent bar would spend 44px of
* an already short viewport on something wanted a few times per session, and the bottom edge is
* already taken by the nav bar and the floating Save button.
* It stays pinned for the whole form. It previously hid on scroll down to save 44px of a short
* viewport, but a bar that is only there when you scroll the wrong way reads as gone — you cannot
* see where you are without scrolling backwards first, which is exactly what the index exists to
* avoid.
*/
export function ProfileFormSectionBar(props: {className?: string}) {
const {className} = props
const {sections, activeId, activeIndex, goTo} = useFormSections()
const [open, setOpen] = useState(false)
const [hidden, setHidden] = useState(false)
const t = useT()
useEffect(() => {
let lastY = window.scrollY
const onScroll = () => {
const y = window.scrollY
// The 8px deadband stops momentum jitter from flickering the bar on and off.
if (Math.abs(y - lastY) > 8) {
setHidden(y > lastY && y > 160)
lastY = y
}
}
window.addEventListener('scroll', onScroll, {passive: true})
return () => window.removeEventListener('scroll', onScroll)
}, [])
if (sections.length === 0) return null
const active = sections[activeIndex]
@@ -169,8 +155,7 @@ export function ProfileFormSectionBar(props: {className?: string}) {
status bar in the Capacitor shell; the variable is 0 in a browser, so one rule covers both. */}
<div
className={clsx(
'bg-canvas-100/95 border-canvas-300 sticky top-[var(--tnh)] z-20 -mx-6 border-b px-6 backdrop-blur transition-transform duration-200',
hidden && '-translate-y-full',
'bg-canvas-100/95 border-canvas-300 sticky top-[var(--tnh)] z-20 -mx-6 border-b px-6 backdrop-blur',
className,
)}
>

View File

@@ -100,7 +100,7 @@ export function ProfileInfo(props: {
Pull it up through the body's safe-area padding and add that inset back as padding, so the
bar's background fills the status-bar strip while its buttons stay clear of the native nav. */}
<div
className="bg-canvas-50/95 border-canvas-300 sticky top-0 z-20 mb-6 flex items-center border-b px-4 sm:px-9 py-4 -mt-[env(safe-area-inset-top)] pt-[calc(1rem+env(safe-area-inset-top))] backdrop-blur"
className="bg-canvas-50/95 border-canvas-300 top-0 z-20 mb-6 flex items-center border-b px-4 sm:px-9 py-4 backdrop-blur"
style={{
borderBottomWidth: '1px',
gap: '10px',

View File

@@ -112,7 +112,9 @@ function ProfilePageInner(props: {user: User; profile: Profile}) {
url={`/profile`}
/>
<Col className="w-full">
<BackButton className="ml-2 mb-2 self-start" />
{/* `ml-4` rather than `ml-2`: the button carries 2 of hit-area padding, so this is what puts
the chevron on the same left edge as the form column's `px-6` below. */}
<BackButton className="ml-4 mb-2 self-start" />
{/* The index sits in the left gutter from `xl` up, where there is room for it beside a form
capped at 3xl. Below that it is hidden rather than stacked: a list of eighteen links above
the first field would be a second thing to read before starting. */}
@@ -120,7 +122,7 @@ function ProfilePageInner(props: {user: User; profile: Profile}) {
<ProfileFormNav className="sticky top-8 hidden h-fit w-52 shrink-0 xl:flex" />
<Col className={'w-full min-w-0 max-w-3xl'}>
{/* Same index, one row instead of a column, wherever the rail does not fit. */}
<ProfileFormSectionBar className="xl:hidden" />
<ProfileFormSectionBar className="mb-4 xl:hidden" />
<RequiredProfileUserForm
data={baseUser}
setData={setBaseUserState}

View File

@@ -146,7 +146,9 @@ export default function SignupPage() {
// the same form gets on /profile, with the page's whole right half empty.
<Col className={'w-full px-6 py-4'}>
<div className="mx-auto w-full max-w-4xl">
<BackButton className="-ml-2 mb-2 self-start" />
{/* No negative margin: the chevron lines up with the column's left edge, like every
other thing on the page. Pulled out by 2 it hung into the page gutter. */}
<BackButton className="mb-2 self-start" />
{/* The flow had no progress indicator at all: two substantial form steps that looked alike,
so the only way to learn there were two was to finish the first. The bar is deliberately
the same component as /onboarding — those three screens lead directly here, and a reader
@@ -158,7 +160,10 @@ export default function SignupPage() {
current: step + 1,
total: TOTAL_STEPS,
})}
className="mb-7"
// Step 2 puts the section bar directly under this. `mb-7` plus the form row's own top
// padding left 44px of empty page between the two, which read as the bar belonging to
// the form below rather than to the header above it.
className={step === 1 ? 'mb-4' : 'mb-7'}
/>
</div>
{step === 0 ? (
@@ -175,10 +180,10 @@ export default function SignupPage() {
// Same index as /profile: this is the same eighteen-section form, and it is longer here
// because nothing is filled in yet. Hidden below `xl`, where there is no gutter to put it
// in without pushing a list of links above the first field.
<div className={'mx-auto mb-2 flex w-full max-w-6xl gap-10 py-4'}>
<div className={'mx-auto mb-2 flex w-full max-w-6xl gap-10 pb-4'}>
<ProfileFormNav className="sticky top-8 hidden h-fit w-52 shrink-0 xl:flex" />
<Col className={'w-full min-w-0 max-w-3xl'}>
<ProfileFormSectionBar className="xl:hidden" />
<ProfileFormSectionBar className="mb-4 xl:hidden" />
<OptionalProfileUserForm
profile={profileForm}
setProfile={setProfileState}