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:
@@ -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>
|
||||||
|
|||||||
@@ -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%]',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user