fix(ux): mobile polish — inputMode=decimal default, dialog padding, more-sheet touch targets

Three audit-pass-#3 mobile findings, all in shared primitives so the
fix lands everywhere those primitives are used.

- Input defaults inputMode='decimal' when type='number' and the caller
  hasn't overridden. Currency/dimension/price fields across invoices,
  expenses, berth specs etc. now show iOS's numeric pad instead of full
  QWERTY. Caller can still pass inputMode='numeric' for integer-only
  fields.
- DialogContent: padding tightens to p-4 on mobile and restores p-6
  at sm+ — the previous fixed p-6 ate ~48px of horizontal width on a
  390px iPhone, crushing form-field space. Also adds a max-h-[100dvh]
  + overflow-y-auto so long modal forms scroll inside the dialog
  instead of pushing the close button off-screen.
- MoreSheet (mobile bottom-tab "More" drawer): grid-cols-3 cells now
  enforce min-h-[88px] so each Apple-HIG-sized 44pt touch target gets
  reliable hit area. Icon size bumped from 6 to 7 for visual weight at
  the larger cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-06 15:16:33 +02:00
parent 9240cf1808
commit 8690352c56
3 changed files with 20 additions and 5 deletions

View File

@@ -65,7 +65,7 @@ export function MoreSheet({
<DrawerHeader> <DrawerHeader>
<DrawerTitle>More</DrawerTitle> <DrawerTitle>More</DrawerTitle>
</DrawerHeader> </DrawerHeader>
<ul className="grid grid-cols-3 gap-1 px-3 pb-4"> <ul className="grid grid-cols-3 gap-2 px-3 pb-4">
{MORE_ITEMS.map((item) => { {MORE_ITEMS.map((item) => {
const Icon = item.icon; const Icon = item.icon;
return ( return (
@@ -74,9 +74,12 @@ export function MoreSheet({
<Link <Link
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
href={`/${portSlug}/${item.segment}` as any} href={`/${portSlug}/${item.segment}` as any}
className="flex flex-col items-center justify-center gap-1.5 rounded-md py-4 text-xs text-foreground hover:bg-accent" // min-h-[88px] guarantees a 44pt vertical touch target
// (Apple HIG); icon + label centered. The grid gap is
// 8px so each cell still has clearance from neighbours.
className="flex min-h-[88px] flex-col items-center justify-center gap-1.5 rounded-md py-3 px-2 text-xs text-foreground hover:bg-accent active:bg-accent/80"
> >
<Icon className="size-6 text-muted-foreground" aria-hidden /> <Icon className="size-7 text-muted-foreground" aria-hidden />
<span className="font-medium">{item.label}</span> <span className="font-medium">{item.label}</span>
</Link> </Link>
</DrawerClose> </DrawerClose>

View File

@@ -43,7 +43,12 @@ const DialogContent = React.forwardRef<
// individually so tailwind-merge doesn't collapse our centering classes. // individually so tailwind-merge doesn't collapse our centering classes.
// (Don't use `inset-0` + `sm:inset-auto` here - twMerge sees that as a // (Don't use `inset-0` + `sm:inset-auto` here - twMerge sees that as a
// conflict and silently strips `sm:left-[50%]` / `sm:top-[50%]`.) // conflict and silently strips `sm:left-[50%]` / `sm:top-[50%]`.)
'fixed top-0 right-0 bottom-0 left-0 z-50 grid w-full gap-4 border-0 bg-background p-6 shadow-lg duration-200', // Padding: tighter on mobile (16px) so multi-field forms keep useful
// working width on a 390px iPhone; original 24px restored at sm+.
// Long forms get an internal scroll cap so the close button + footer
// stay reachable without scrolling the whole page.
'fixed top-0 right-0 bottom-0 left-0 z-50 grid w-full gap-4 border-0 bg-background p-4 shadow-lg duration-200 sm:p-6',
'max-h-[100dvh] overflow-y-auto sm:max-h-[calc(100dvh-2rem)]',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'sm:top-[50%] sm:right-auto sm:bottom-auto sm:left-[50%] sm:max-w-lg sm:translate-x-[-50%] sm:translate-y-[-50%] sm:border sm:rounded-lg', 'sm:top-[50%] sm:right-auto sm:bottom-auto sm:left-[50%] sm:max-w-lg sm:translate-x-[-50%] sm:translate-y-[-50%] sm:border sm:rounded-lg',
'sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%]', 'sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%]',

View File

@@ -3,10 +3,17 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => { ({ className, type, inputMode, ...props }, ref) => {
// Default `type=number` to a numeric keyboard on mobile when the caller
// didn't explicitly override `inputMode`. Without this, iOS shows the
// full QWERTY keyboard for prices/dimensions/etc. — common audit gripe.
// `decimal` covers both whole numbers and decimals; if the caller wants
// strict integer input they pass `inputMode="numeric"` explicitly.
const resolvedInputMode = inputMode ?? (type === 'number' ? 'decimal' : undefined);
return ( return (
<input <input
type={type} type={type}
inputMode={resolvedInputMode}
className={cn( className={cn(
'flex h-11 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', 'flex h-11 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className, className,