فهرست منبع

Pin the header and footer of the vehicle dialog so Save never scrolls out of view (#501)

* Pin the header and footer of the vehicle dialog so Save never scrolls out of view

DialogContent scrolled as a whole, so on a laptop the Save and Cancel buttons of a long
form such as Edit vehicle ended up below the fold. The shared dialog gets a DialogBody
slot: when one is present, only the body scrolls and the header and footer stay put,
with a soft scroll-edge shadow that appears only while content is hidden. Edit vehicle
adopts it, grows to 5xl, and names the open vehicle under the title.

Co-Authored-By: Claude <noreply@anthropic.com>

* Give the customer dialog the same pinned header and footer

Add and Edit customer scrolled the whole dialog too. The form moves onto the
DialogBody slot, grows to 5xl, and names the open customer under the title.

Co-Authored-By: Claude <noreply@anthropic.com>

* Keep the vehicle picker still while searching, and pick with the keyboard

The result list had no fixed height, so every keystroke that changed the match
count, and every swap between spinner, rows and the empty message, resized the
dialog and re-centred it under the cursor. The search box now sits pinned in the
header above a fixed-height body, a search in flight runs the thin progress bar
under the box while the old rows stay put, and the arrow keys and Enter pick a
vehicle from the search box.

Co-Authored-By: Claude <noreply@anthropic.com>

* Pin the header and footer of the six other dialogs that scrolled as a whole

Inventory part, recurring invoice, service record, dashboard custom card, labor
preset and custom field dialogs move onto the DialogBody slot, so their Save and
Cancel buttons stay in view while the fields scroll. The labor preset's mode tabs
sit in the pinned header with the title.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Bernt Christian Egeland 3 روز پیش
والد
کامیت
5efb82ca27

+ 17 - 0
src/app/globals.css

@@ -413,6 +413,23 @@ html body[data-scroll-locked] {
   display: none;
 }
 
+/* Scroll-edge shadows: a soft shadow at the top or bottom edge only while content is
+   hidden in that direction. Pure CSS: the covering gradients scroll with the content
+   (attachment: local) and slide off the fixed shadows as soon as there is more to see. */
+.scroll-shadows {
+  --scroll-shadow: color-mix(in oklab, var(--foreground) 9%, transparent);
+  background:
+    linear-gradient(var(--background) 40%, transparent) center top / 100% 32px local,
+    linear-gradient(transparent, var(--background) 60%) center bottom / 100% 32px local,
+    radial-gradient(farthest-side at 50% 0, var(--scroll-shadow), transparent) center top / 100% 12px scroll,
+    radial-gradient(farthest-side at 50% 100%, var(--scroll-shadow), transparent) center bottom / 100% 12px scroll;
+  background-repeat: no-repeat;
+  background-color: var(--background);
+}
+.dark .scroll-shadows {
+  --scroll-shadow: color-mix(in oklab, var(--foreground) 14%, transparent);
+}
+
 /* Custom scrollbar */
 ::-webkit-scrollbar {
   width: 6px;

+ 28 - 2
src/components/ui/dialog.tsx

@@ -54,6 +54,8 @@ function DialogContent({
         data-slot="dialog-content"
         className={cn(
           'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-3rem)] max-h-[85vh] overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg',
+          // With a DialogBody inside, only the body scrolls: the header and footer stay put.
+          'group/dialog has-data-[slot=dialog-body]:flex has-data-[slot=dialog-body]:flex-col has-data-[slot=dialog-body]:gap-0 has-data-[slot=dialog-body]:overflow-hidden has-data-[slot=dialog-body]:p-0',
           className
         )}
         {...props}
@@ -77,7 +79,26 @@ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
   return (
     <div
       data-slot="dialog-header"
-      className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
+      className={cn(
+        'flex flex-col gap-2 text-center sm:text-left',
+        'group-has-data-[slot=dialog-body]/dialog:border-b group-has-data-[slot=dialog-body]/dialog:px-6 group-has-data-[slot=dialog-body]/dialog:pt-6 group-has-data-[slot=dialog-body]/dialog:pb-4',
+        className
+      )}
+      {...props}
+    />
+  )
+}
+
+/**
+ * The scrolling middle of a long dialog. Put the fields here, with DialogHeader above and
+ * DialogFooter below, and the dialog pins both while this part scrolls, so the buttons are
+ * always in reach. A soft shadow on the top or bottom edge says there is more to see.
+ */
+function DialogBody({ className, ...props }: React.ComponentProps<'div'>) {
+  return (
+    <div
+      data-slot="dialog-body"
+      className={cn('scroll-shadows min-h-0 flex-1 overflow-y-auto px-6 py-5', className)}
       {...props}
     />
   )
@@ -94,7 +115,11 @@ function DialogFooter({
   return (
     <div
       data-slot="dialog-footer"
-      className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
+      className={cn(
+        'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
+        'group-has-data-[slot=dialog-body]/dialog:border-t group-has-data-[slot=dialog-body]/dialog:px-6 group-has-data-[slot=dialog-body]/dialog:py-4',
+        className
+      )}
       {...props}
     >
       {children}
@@ -132,6 +157,7 @@ function DialogDescription({
 
 export {
   Dialog,
+  DialogBody,
   DialogClose,
   DialogContent,
   DialogDescription,

+ 106 - 65
src/components/vehicle-picker-dialog.tsx

@@ -1,11 +1,19 @@
 'use client'
 
-import { useState, useEffect, useRef } from 'react'
+import { useState, useEffect, useRef, type KeyboardEvent } from 'react'
 import { useRouter } from 'next/navigation'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import {
+  Dialog,
+  DialogBody,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
+import { cn } from '@/lib/utils'
 import { Loader2, Plus, Car, Search, ArrowLeft, UserPlus, ShoppingCart, Ship } from 'lucide-react'
 import { createVehicle, searchVehicles } from '@/features/vehicles/Actions/vehicleActions'
 import { createDraftCounterSale } from '@/features/vehicles/Actions/createDraftServiceRecord'
@@ -89,6 +97,7 @@ export function VehiclePickerDialog({
           const result = await searchVehicles(query || undefined, PAGE_SIZE)
           if (request !== requestRef.current) return
           setFilteredVehicles(result.success && result.data ? result.data : [])
+          setActiveIndex(0)
         } catch {
           if (request === requestRef.current) setFilteredVehicles([])
         } finally {
@@ -100,9 +109,35 @@ export function VehiclePickerDialog({
     return () => clearTimeout(timer)
   }, [open, pickerStep, vehicleSearch])
 
+  // The arrow keys walk the list from the search box and Enter picks the highlighted row,
+  // so the common case is type, Enter, done, without reaching for the mouse.
+  const [activeIndex, setActiveIndex] = useState(0)
+  const listRef = useRef<HTMLDivElement>(null)
+  useEffect(() => {
+    listRef.current
+      ?.querySelector<HTMLElement>('[data-active="true"]')
+      ?.scrollIntoView?.({ block: 'nearest' })
+  }, [activeIndex])
+
+  const handleSearchKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
+    if (filteredVehicles.length === 0) return
+    if (e.key === 'ArrowDown') {
+      e.preventDefault()
+      setActiveIndex((i) => Math.min(i + 1, filteredVehicles.length - 1))
+    } else if (e.key === 'ArrowUp') {
+      e.preventDefault()
+      setActiveIndex((i) => Math.max(i - 1, 0))
+    } else if (e.key === 'Enter') {
+      e.preventDefault()
+      const active = filteredVehicles[activeIndex]
+      if (active) handleSelect(active.id)
+    }
+  }
+
   const resetState = () => {
     setPickerStep('select')
     setVehicleSearch('')
+    setActiveIndex(0)
     setNewMake('')
     setNewModel('')
     setNewYear('')
@@ -229,78 +264,84 @@ export function VehiclePickerDialog({
           <>
             <DialogHeader>
               <DialogTitle>{displayTitle}</DialogTitle>
-            </DialogHeader>
-            <div className="space-y-3">
-              <div className="relative">
-                <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
+              {/* The search box is pinned above the list, so it never moves under the cursor */}
+              <div className="relative mt-1">
+                <Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
                 <Input
                   placeholder={isMarine ? t('searchPlaceholderMarine') : t('searchPlaceholder')}
                   value={vehicleSearch}
                   onChange={(e) => setVehicleSearch(e.target.value)}
+                  onKeyDown={handleSearchKeyDown}
                   className="pl-9"
                   autoFocus
                 />
-              </div>
-              <div className="max-h-[300px] overflow-y-auto">
-                {vehiclesLoading && filteredVehicles.length === 0 ? (
-                  <div className="flex justify-center py-6">
-                    <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
-                  </div>
-                ) : filteredVehicles.length === 0 ? (
-                  <div className="py-6 text-center space-y-3">
-                    <p className="text-sm text-muted-foreground">
-                      {!vehicleSearch.trim()
-                        ? isMarine
-                          ? t('emptyMarine')
-                          : t('empty')
-                        : isMarine
-                          ? t('emptySearchMarine')
-                          : t('emptySearch')}
-                    </p>
-                    <Button variant="outline" onClick={() => setPickerStep('create')}>
-                      <Plus className="h-4 w-4 mr-1" />
-                      {isMarine ? t('addNewVehicleMarine') : t('addNewVehicle')}
-                    </Button>
-                  </div>
-                ) : (
-                  <div className="space-y-1">
-                    {filteredVehicles.map((v) => (
-                      <button
-                        key={v.id}
-                        type="button"
-                        className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors hover:bg-muted"
-                        onClick={() => handleSelect(v.id)}
-                      >
-                        {isMarine ? (
-                          <Ship className="h-4 w-4 shrink-0 text-muted-foreground" />
-                        ) : (
-                          <Car className="h-4 w-4 shrink-0 text-muted-foreground" />
-                        )}
-                        <div className="min-w-0 flex-1">
-                          <p className="text-sm font-medium">
-                            {v.year} {v.make} {v.model}
-                          </p>
-                          <p className="text-xs text-muted-foreground">
-                            {[v.licensePlate, v.customer?.name].filter(Boolean).join(' · ') ||
-                              (isMarine ? t('noPlateMarine') : t('noPlate'))}
-                          </p>
-                        </div>
-                      </button>
-                    ))}
-                  </div>
+                {/* A search in flight runs a thin bar under the box; the old rows stay in place */}
+                {vehiclesLoading && filteredVehicles.length > 0 && (
+                  <span className="search-progress" aria-hidden="true" />
                 )}
               </div>
-              <div className="flex gap-2">
-                <Button variant="ghost" className="flex-1" onClick={() => setPickerStep('create')}>
-                  <Plus className="h-4 w-4 mr-1" />
-                  {isMarine ? t('addNewVehicleMarine') : t('addNewVehicle')}
-                </Button>
-                <Button variant="ghost" className="flex-1" onClick={() => setPickerStep('sale')}>
-                  <ShoppingCart className="h-4 w-4 mr-1" />
-                  {t('partsSale')}
-                </Button>
-              </div>
-            </div>
+            </DialogHeader>
+            {/* One fixed height whatever the list holds, so the dialog never resizes or
+                re-centres while results, a spinner and the empty message take turns */}
+            <DialogBody ref={listRef} className="h-80 p-2">
+              {vehiclesLoading && filteredVehicles.length === 0 ? (
+                <div className="flex h-full items-center justify-center">
+                  <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
+                </div>
+              ) : filteredVehicles.length === 0 ? (
+                <div className="flex h-full items-center justify-center px-6 text-center">
+                  <p className="text-sm text-muted-foreground">
+                    {!vehicleSearch.trim()
+                      ? isMarine
+                        ? t('emptyMarine')
+                        : t('empty')
+                      : isMarine
+                        ? t('emptySearchMarine')
+                        : t('emptySearch')}
+                  </p>
+                </div>
+              ) : (
+                <div
+                  className={cn('space-y-0.5 transition-opacity', vehiclesLoading && 'opacity-60')}
+                >
+                  {filteredVehicles.map((v, index) => (
+                    <button
+                      key={v.id}
+                      type="button"
+                      data-active={index === activeIndex}
+                      className="flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors data-[active=true]:bg-muted"
+                      onMouseMove={() => setActiveIndex(index)}
+                      onClick={() => handleSelect(v.id)}
+                    >
+                      {isMarine ? (
+                        <Ship className="h-4 w-4 shrink-0 text-muted-foreground" />
+                      ) : (
+                        <Car className="h-4 w-4 shrink-0 text-muted-foreground" />
+                      )}
+                      <div className="min-w-0 flex-1">
+                        <p className="truncate text-sm font-medium">
+                          {v.year} {v.make} {v.model}
+                        </p>
+                        <p className="truncate text-xs text-muted-foreground">
+                          {[v.licensePlate, v.customer?.name].filter(Boolean).join(' · ') ||
+                            (isMarine ? t('noPlateMarine') : t('noPlate'))}
+                        </p>
+                      </div>
+                    </button>
+                  ))}
+                </div>
+              )}
+            </DialogBody>
+            <DialogFooter className="flex-row">
+              <Button variant="ghost" className="flex-1" onClick={() => setPickerStep('create')}>
+                <Plus className="h-4 w-4 mr-1" />
+                {isMarine ? t('addNewVehicleMarine') : t('addNewVehicle')}
+              </Button>
+              <Button variant="ghost" className="flex-1" onClick={() => setPickerStep('sale')}>
+                <ShoppingCart className="h-4 w-4 mr-1" />
+                {t('partsSale')}
+              </Button>
+            </DialogFooter>
           </>
         ) : pickerStep === 'sale' ? (
           <>

+ 13 - 6
src/features/billing/Components/RecurringInvoicesClient.tsx

@@ -16,7 +16,14 @@ import {
   TableHeader,
   TableRow,
 } from '@/components/ui/table'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import {
+  Dialog,
+  DialogBody,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
 import { Input } from '@/components/ui/input'
 import { DateInput } from '@/components/ui/date-input'
 import { Label } from '@/components/ui/label'
@@ -564,11 +571,11 @@ export default function RecurringInvoicesClient({
 
       {/* Create dialog */}
       <Dialog open={showCreate} onOpenChange={setShowCreate}>
-        <DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
+        <DialogContent className="max-h-[85vh] sm:max-w-2xl">
           <DialogHeader>
             <DialogTitle>{t('recurring.dialogTitle')}</DialogTitle>
           </DialogHeader>
-          <div className="space-y-4">
+          <DialogBody className="space-y-4">
             {/* Basic info */}
             <div className="grid gap-3 sm:grid-cols-2">
               <div className="space-y-1.5">
@@ -805,7 +812,8 @@ export default function RecurringInvoicesClient({
             </Card>
 
             {/* Actions */}
-            <div className="flex justify-end gap-2 pt-2">
+          </DialogBody>
+          <DialogFooter>
               <Button
                 variant="outline"
                 onClick={() => {
@@ -819,8 +827,7 @@ export default function RecurringInvoicesClient({
                 {isPending && <Loader2 className="h-4 w-4 animate-spin mr-1.5" />}
                 {t('recurring.create')}
               </Button>
-            </div>
-          </div>
+          </DialogFooter>
         </DialogContent>
       </Dialog>
     </div>

+ 13 - 6
src/features/custom-fields/Components/CustomFieldsManager.tsx

@@ -17,7 +17,14 @@ import {
   SelectTrigger,
   SelectValue,
 } from '@/components/ui/select'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import {
+  Dialog,
+  DialogBody,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
 import { Textarea } from '@/components/ui/textarea'
 import { Checkbox } from '@/components/ui/checkbox'
 import { DateInput } from '@/components/ui/date-input'
@@ -355,7 +362,7 @@ export function CustomFieldsManager({
           if (!open) resetForm()
         }}
       >
-        <DialogContent className="max-w-md max-h-[85vh] overflow-y-auto">
+        <DialogContent className="max-h-[85vh] sm:max-w-md">
           <DialogHeader>
             <DialogTitle>
               {editing ? t('customFields.editField') : t('customFields.newField')}
@@ -366,7 +373,7 @@ export function CustomFieldsManager({
               className="self-start"
             />
           </DialogHeader>
-          <div className="space-y-4">
+          <DialogBody className="space-y-4">
             <div className="space-y-2">
               <Label>{t('customFields.label')}</Label>
               <Input
@@ -573,7 +580,8 @@ export function CustomFieldsManager({
               </div>
             </div>
 
-            <div className="flex justify-end gap-2">
+          </DialogBody>
+          <DialogFooter>
               <Button
                 variant="ghost"
                 onClick={() => {
@@ -591,8 +599,7 @@ export function CustomFieldsManager({
                 {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                 {editing ? t('customFields.update') : t('customFields.create')}
               </Button>
-            </div>
-          </div>
+          </DialogFooter>
         </DialogContent>
       </Dialog>
     </div>

+ 150 - 140
src/features/customers/Components/CustomerForm.tsx

@@ -20,8 +20,10 @@ import {
 import { listDesignOptions } from '@/features/invoice-designer/Actions/documentDesignActions'
 import {
   Dialog,
+  DialogBody,
   DialogContent,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogTitle,
 } from '@/components/ui/dialog'
@@ -193,14 +195,20 @@ export function CustomerForm({
     setLoading(false)
   }
 
+  // "Kari Nordmann · Nordmann AS": who is open, under the title, when editing
+  const customerIdentity = customer
+    ? [customer.name, customer.company].filter(Boolean).join(' · ')
+    : null
+
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl">
+      <DialogContent className="max-h-[90vh] sm:max-w-5xl">
         <DialogHeader>
           <DialogTitle>{customer ? t('editTitle') : t('addTitle')}</DialogTitle>
           <DocsLink href="/docs/features/customers" variant="hint" className="self-start" />
-          <DialogDescription className="sr-only">
-            {customer ? t('editTitle') : t('addTitle')}
+          {/* Which customer is open, so the title line answers it before the fields do */}
+          <DialogDescription className={customerIdentity ? undefined : 'sr-only'}>
+            {customerIdentity ?? (customer ? t('editTitle') : t('addTitle'))}
           </DialogDescription>
         </DialogHeader>
 
@@ -208,168 +216,170 @@ export function CustomerForm({
           key={customer?.id ?? `${defaults?.name ?? ''}|${defaults?.address ?? ''}`}
           ref={formRef}
           onSubmit={handleSubmit}
-          className="grid gap-x-6 gap-y-4 md:grid-cols-2"
+          className="flex min-h-0 flex-1 flex-col"
         >
-          {/* The keeper on a registration document is a customer waiting to be typed in */}
-          <div className="space-y-3 md:col-span-2">
-            <ScanDocumentButton onScanned={applyScan} />
+          <DialogBody className="grid content-start gap-x-8 gap-y-4 md:grid-cols-2">
+            {/* The keeper on a registration document is a customer waiting to be typed in */}
+            <div className="space-y-3 md:col-span-2">
+              <ScanDocumentButton onScanned={applyScan} />
 
-            {scannedVehicle && (
-              <label className="flex cursor-pointer items-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-3">
-                <Checkbox
-                  checked={addVehicle}
-                  onCheckedChange={(next) => setAddVehicle(next === true)}
-                />
-                <span className="text-sm">
-                  {t('addScannedVehicle', { vehicle: vehicleLabel(scannedVehicle) })}
-                </span>
-              </label>
-            )}
-          </div>
+              {scannedVehicle && (
+                <label className="flex cursor-pointer items-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-3">
+                  <Checkbox
+                    checked={addVehicle}
+                    onCheckedChange={(next) => setAddVehicle(next === true)}
+                  />
+                  <span className="text-sm">
+                    {t('addScannedVehicle', { vehicle: vehicleLabel(scannedVehicle) })}
+                  </span>
+                </label>
+              )}
+            </div>
 
-          {/* Left: who the customer is and how to reach them */}
-          <div className="space-y-4">
-            <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
-              {t('sectionContact')}
-            </p>
+            {/* Left: who the customer is and how to reach them */}
+            <div className="space-y-4">
+              <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
+                {t('sectionContact')}
+              </p>
 
-            <div className="grid grid-cols-[1fr_120px] gap-4">
-              <div className="space-y-2">
-                <Label htmlFor="name">{t('nameRequired')}</Label>
-                <Input
-                  id="name"
-                  name="name"
-                  placeholder={t('namePlaceholder')}
-                  defaultValue={customer?.name ?? defaults?.name ?? ''}
-                  required
-                />
+              <div className="grid grid-cols-[1fr_120px] gap-4">
+                <div className="space-y-2">
+                  <Label htmlFor="name">{t('nameRequired')}</Label>
+                  <Input
+                    id="name"
+                    name="name"
+                    placeholder={t('namePlaceholder')}
+                    defaultValue={customer?.name ?? defaults?.name ?? ''}
+                    required
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="customerNumber">{t('customerNumber')}</Label>
+                  <Input
+                    id="customerNumber"
+                    name="customerNumber"
+                    placeholder={t('customerNumberAuto')}
+                    defaultValue={customer?.customerNumber ?? ''}
+                    maxLength={20}
+                  />
+                </div>
+              </div>
+
+              <div className="grid grid-cols-2 gap-4">
+                <div className="space-y-2">
+                  <Label htmlFor="email">{tc('form.email')}</Label>
+                  <Input
+                    id="email"
+                    name="email"
+                    type="email"
+                    placeholder={t('emailPlaceholder')}
+                    defaultValue={customer?.email ?? ''}
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="phone">{tc('form.phone')}</Label>
+                  <Input
+                    id="phone"
+                    name="phone"
+                    placeholder={t('phonePlaceholder')}
+                    defaultValue={customer?.phone ?? ''}
+                  />
+                </div>
               </div>
+
               <div className="space-y-2">
-                <Label htmlFor="customerNumber">{t('customerNumber')}</Label>
-                <Input
-                  id="customerNumber"
-                  name="customerNumber"
-                  placeholder={t('customerNumberAuto')}
-                  defaultValue={customer?.customerNumber ?? ''}
-                  maxLength={20}
+                <Label htmlFor="address">{tc('form.address')}</Label>
+                {/* An address is street, postcode and town, and it prints on
+                  the invoice the way it is typed here. One row until there
+                  is more, so the form does not open with a tall empty box. */}
+                <Textarea
+                  id="address"
+                  name="address"
+                  placeholder={t('addressPlaceholder')}
+                  rows={2}
+                  className="min-h-9 resize-none"
+                  defaultValue={customer?.address ?? defaults?.address ?? ''}
                 />
               </div>
             </div>
 
-            <div className="grid grid-cols-2 gap-4">
+            {/* Right: who they are on an invoice */}
+            <div className="space-y-4">
+              <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
+                {t('sectionBilling')}
+              </p>
+
               <div className="space-y-2">
-                <Label htmlFor="email">{tc('form.email')}</Label>
+                <Label htmlFor="company">{tc('form.company')}</Label>
                 <Input
-                  id="email"
-                  name="email"
-                  type="email"
-                  placeholder={t('emailPlaceholder')}
-                  defaultValue={customer?.email ?? ''}
+                  id="company"
+                  name="company"
+                  placeholder={t('companyPlaceholder')}
+                  defaultValue={customer?.company ?? ''}
                 />
               </div>
+
               <div className="space-y-2">
-                <Label htmlFor="phone">{tc('form.phone')}</Label>
+                <Label htmlFor="taxId">{t('taxId')}</Label>
                 <Input
-                  id="phone"
-                  name="phone"
-                  placeholder={t('phonePlaceholder')}
-                  defaultValue={customer?.phone ?? ''}
+                  id="taxId"
+                  name="taxId"
+                  placeholder={t('taxIdPlaceholder')}
+                  defaultValue={customer?.taxId ?? ''}
                 />
+                <p className="text-xs text-muted-foreground">{t('taxIdHint')}</p>
               </div>
-            </div>
-
-            <div className="space-y-2">
-              <Label htmlFor="address">{tc('form.address')}</Label>
-              {/* An address is street, postcode and town, and it prints on
-                  the invoice the way it is typed here. One row until there
-                  is more, so the form does not open with a tall empty box. */}
-              <Textarea
-                id="address"
-                name="address"
-                placeholder={t('addressPlaceholder')}
-                rows={2}
-                className="min-h-9 resize-none"
-                defaultValue={customer?.address ?? defaults?.address ?? ''}
-              />
-            </div>
-          </div>
-
-          {/* Right: who they are on an invoice */}
-          <div className="space-y-4">
-            <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
-              {t('sectionBilling')}
-            </p>
 
-            <div className="space-y-2">
-              <Label htmlFor="company">{tc('form.company')}</Label>
-              <Input
-                id="company"
-                name="company"
-                placeholder={t('companyPlaceholder')}
-                defaultValue={customer?.company ?? ''}
-              />
-            </div>
-
-            <div className="space-y-2">
-              <Label htmlFor="taxId">{t('taxId')}</Label>
-              <Input
-                id="taxId"
-                name="taxId"
-                placeholder={t('taxIdPlaceholder')}
-                defaultValue={customer?.taxId ?? ''}
-              />
-              <p className="text-xs text-muted-foreground">{t('taxIdHint')}</p>
-            </div>
-
-            <div className="flex items-center justify-between gap-4 rounded-lg border p-3">
-              <div className="space-y-0.5">
-                <Label>{t('taxExempt')}</Label>
-                <p className="text-xs text-muted-foreground">{t('taxExemptHint')}</p>
+              <div className="flex items-center justify-between gap-4 rounded-lg border p-3">
+                <div className="space-y-0.5">
+                  <Label>{t('taxExempt')}</Label>
+                  <p className="text-xs text-muted-foreground">{t('taxExemptHint')}</p>
+                </div>
+                <Switch checked={taxExempt} onCheckedChange={setTaxExempt} />
               </div>
-              <Switch checked={taxExempt} onCheckedChange={setTaxExempt} />
-            </div>
 
-            {designOptions.length > 0 && (
-              <div className="space-y-2">
-                <Label>{t('invoiceDesign')}</Label>
-                <Select value={invoiceDesignId} onValueChange={setInvoiceDesignId}>
-                  <SelectTrigger className="w-full">
-                    <SelectValue />
-                  </SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value={FOLLOW_DEFAULT}>{t('invoiceDesignDefault')}</SelectItem>
-                    {designOptions.map((option) => (
-                      <SelectItem key={option.id} value={option.id}>
-                        {option.name}
-                      </SelectItem>
-                    ))}
-                  </SelectContent>
-                </Select>
-                <p className="text-xs text-muted-foreground">{t('invoiceDesignHint')}</p>
-              </div>
-            )}
+              {designOptions.length > 0 && (
+                <div className="space-y-2">
+                  <Label>{t('invoiceDesign')}</Label>
+                  <Select value={invoiceDesignId} onValueChange={setInvoiceDesignId}>
+                    <SelectTrigger className="w-full">
+                      <SelectValue />
+                    </SelectTrigger>
+                    <SelectContent>
+                      <SelectItem value={FOLLOW_DEFAULT}>{t('invoiceDesignDefault')}</SelectItem>
+                      {designOptions.map((option) => (
+                        <SelectItem key={option.id} value={option.id}>
+                          {option.name}
+                        </SelectItem>
+                      ))}
+                    </SelectContent>
+                  </Select>
+                  <p className="text-xs text-muted-foreground">{t('invoiceDesignHint')}</p>
+                </div>
+              )}
 
-            <div className="flex items-center justify-between gap-4 rounded-lg border p-3">
-              <div className="space-y-0.5">
-                <Label>{t('reminderOptOut')}</Label>
-                <p className="text-xs text-muted-foreground">{t('reminderOptOutHint')}</p>
+              <div className="flex items-center justify-between gap-4 rounded-lg border p-3">
+                <div className="space-y-0.5">
+                  <Label>{t('reminderOptOut')}</Label>
+                  <p className="text-xs text-muted-foreground">{t('reminderOptOutHint')}</p>
+                </div>
+                <Switch checked={reminderOptOut} onCheckedChange={setReminderOptOut} />
               </div>
-              <Switch checked={reminderOptOut} onCheckedChange={setReminderOptOut} />
             </div>
-          </div>
 
-          <div className="space-y-2 md:col-span-2">
-            <Label htmlFor="notes">{tc('form.notes')}</Label>
-            <Textarea
-              id="notes"
-              name="notes"
-              placeholder={t('notesPlaceholder')}
-              rows={3}
-              defaultValue={customer?.notes ?? ''}
-            />
-          </div>
+            <div className="space-y-2 md:col-span-2">
+              <Label htmlFor="notes">{tc('form.notes')}</Label>
+              <Textarea
+                id="notes"
+                name="notes"
+                placeholder={t('notesPlaceholder')}
+                rows={3}
+                defaultValue={customer?.notes ?? ''}
+              />
+            </div>
+          </DialogBody>
 
-          <div className="flex justify-end gap-3 border-t pt-4 md:col-span-2">
+          <DialogFooter>
             <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
               {tc('buttons.cancel')}
             </Button>
@@ -377,7 +387,7 @@ export function CustomerForm({
               {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
               {customer ? tc('buttons.saveChanges') : t('addTitle')}
             </Button>
-          </div>
+          </DialogFooter>
         </form>
       </DialogContent>
     </Dialog>

+ 13 - 6
src/features/dashboard/Components/CustomCardDialog.tsx

@@ -7,7 +7,14 @@ import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Checkbox } from '@/components/ui/checkbox'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import {
+  Dialog,
+  DialogBody,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
 import { DocsLink } from '@/components/docs-link'
 import {
   Select,
@@ -177,7 +184,7 @@ export function CustomCardDialog({
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
       <DialogContent
-        className="max-h-[90vh] overflow-y-auto sm:max-w-2xl"
+        className="max-h-[90vh] sm:max-w-2xl"
         aria-describedby={undefined}
       >
         <DialogHeader>
@@ -185,7 +192,7 @@ export function CustomCardDialog({
           <DocsLink href="/docs/features/dashboard" variant="hint" className="self-start" />
         </DialogHeader>
 
-        <div className="space-y-5">
+        <DialogBody className="space-y-5">
           <div className="grid gap-4 sm:grid-cols-2">
             <div className="space-y-2">
               <Label htmlFor="card-name">{t('cardName')}</Label>
@@ -303,7 +310,8 @@ export function CustomCardDialog({
             </Select>
           </div>
 
-          <div className="flex justify-end gap-3 pt-2">
+        </DialogBody>
+        <DialogFooter>
             <Button variant="outline" onClick={() => onOpenChange(false)}>
               {t('cancel')}
             </Button>
@@ -315,8 +323,7 @@ export function CustomCardDialog({
               {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
               {t('save')}
             </Button>
-          </div>
-        </div>
+        </DialogFooter>
       </DialogContent>
     </Dialog>
   )

+ 9 - 4
src/features/inventory/Components/InventoryPartForm.tsx

@@ -11,8 +11,10 @@ import { Label } from '@/components/ui/label'
 import { Textarea } from '@/components/ui/textarea'
 import {
   Dialog,
+  DialogBody,
   DialogContent,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogTitle,
 } from '@/components/ui/dialog'
@@ -406,7 +408,7 @@ export function InventoryPartForm({
 
   return (
     <Dialog open={open} onOpenChange={handleOpenChange}>
-      <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-3xl">
+      <DialogContent className="max-h-[90vh] sm:max-w-3xl">
         <DialogHeader>
           <div className="flex items-center justify-between gap-4 pr-6">
             <DialogTitle>{part ? t('form.editPart') : t('form.addNewPart')}</DialogTitle>
@@ -428,7 +430,8 @@ export function InventoryPartForm({
           </DialogDescription>
         </DialogHeader>
 
-        <form ref={formRef} onSubmit={handleSubmit} className="space-y-4">
+        <form ref={formRef} onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
+          <DialogBody className="space-y-4">
           {/* Supplier Link */}
           <div className="space-y-2">
             <Label htmlFor="supplierUrl">{t('form.supplierLink')}</Label>
@@ -859,7 +862,9 @@ export function InventoryPartForm({
             />
           </div>
 
-          <div className="flex justify-end gap-3 pt-4">
+          </DialogBody>
+
+          <DialogFooter>
             <Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
               {t('form.cancel')}
             </Button>
@@ -867,7 +872,7 @@ export function InventoryPartForm({
               {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
               {part ? t('form.saveChanges') : t('form.addPart')}
             </Button>
-          </div>
+          </DialogFooter>
         </form>
       </DialogContent>
     </Dialog>

+ 19 - 9
src/features/labor-presets/Components/LaborPresetForm.tsx

@@ -7,7 +7,14 @@ import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Textarea } from '@/components/ui/textarea'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import {
+  Dialog,
+  DialogBody,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
 import { DocsLink } from '@/components/docs-link'
 import { useGlassModal } from '@/components/glass-modal'
 import { createLaborPreset, updateLaborPreset } from '../Actions/laborPresetActions'
@@ -367,7 +374,7 @@ export function LaborPresetForm({
   return (
     <Dialog open={open} onOpenChange={handleOpenChange}>
       <DialogContent
-        className="max-h-[90vh] overflow-y-auto sm:max-w-2xl"
+        className="max-h-[90vh] sm:max-w-2xl"
         aria-describedby={undefined}
       >
         <DialogHeader>
@@ -377,10 +384,9 @@ export function LaborPresetForm({
             variant="hint"
             className="self-start"
           />
-        </DialogHeader>
 
-        {/* Mode tabs */}
-        <div className="flex rounded-lg border p-0.5 bg-muted/50">
+          {/* Mode tabs, pinned with the title so the switch is always in reach */}
+          <div className="mt-1 flex rounded-lg border bg-muted/50 p-0.5">
           <button
             type="button"
             className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
@@ -403,9 +409,11 @@ export function LaborPresetForm({
           >
             {t('form.package')}
           </button>
-        </div>
+          </div>
+        </DialogHeader>
 
-        <form onSubmit={handleSubmit} className="space-y-4">
+        <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
+          <DialogBody className="space-y-4">
           {mode === 'single' ? (
             <div className="space-y-2">
               <div className="hidden grid-cols-[2fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
@@ -590,7 +598,9 @@ export function LaborPresetForm({
             </>
           )}
 
-          <div className="flex justify-end gap-3 pt-4">
+          </DialogBody>
+
+          <DialogFooter>
             <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
               {t('form.cancel')}
             </Button>
@@ -598,7 +608,7 @@ export function LaborPresetForm({
               {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
               {preset ? t('form.saveChanges') : t('form.create')}
             </Button>
-          </div>
+          </DialogFooter>
         </form>
       </DialogContent>
     </Dialog>

+ 15 - 5
src/features/vehicles/Components/ServiceForm.tsx

@@ -14,7 +14,14 @@ import {
   SelectTrigger,
   SelectValue,
 } from '@/components/ui/select'
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import {
+  Dialog,
+  DialogBody,
+  DialogContent,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog'
 import { useGlassModal } from '@/components/glass-modal'
 import { createServiceRecord } from '../Actions/serviceActions'
 import { Loader2 } from 'lucide-react'
@@ -63,12 +70,13 @@ export function ServiceForm({ vehicleId, open, onOpenChange }: ServiceFormProps)
 
   return (
     <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
+      <DialogContent className="max-h-[90vh] sm:max-w-lg">
         <DialogHeader>
           <DialogTitle>Add Service Record</DialogTitle>
         </DialogHeader>
 
-        <form onSubmit={handleSubmit} className="space-y-4">
+        <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
+          <DialogBody className="space-y-4">
           <div className="space-y-2">
             <Label htmlFor="title">Title *</Label>
             <Input id="title" name="title" placeholder="Oil Change" required />
@@ -142,7 +150,9 @@ export function ServiceForm({ vehicleId, open, onOpenChange }: ServiceFormProps)
             />
           </div>
 
-          <div className="flex justify-end gap-3 pt-4">
+          </DialogBody>
+
+          <DialogFooter>
             <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
               Cancel
             </Button>
@@ -150,7 +160,7 @@ export function ServiceForm({ vehicleId, open, onOpenChange }: ServiceFormProps)
               {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
               Add Record
             </Button>
-          </div>
+          </DialogFooter>
         </form>
       </DialogContent>
     </Dialog>

+ 353 - 343
src/features/vehicles/Components/VehicleForm.tsx

@@ -19,8 +19,10 @@ import {
 import { Checkbox } from '@/components/ui/checkbox'
 import {
   Dialog,
+  DialogBody,
   DialogContent,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogTitle,
 } from '@/components/ui/dialog'
@@ -482,403 +484,411 @@ export function VehicleForm({
     }
   }
 
+  // "AB 12345 · Volvo V70 2016": which car is open, under the title, when editing
+  const vehicleIdentity = vehicle
+    ? [vehicle.licensePlate, `${vehicle.make} ${vehicle.model} ${vehicle.year}`.trim()]
+        .filter(Boolean)
+        .join(' · ')
+    : null
+
   return (
     <>
       <Dialog open={open} onOpenChange={onOpenChange}>
-        <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl">
+        <DialogContent className="max-h-[90vh] sm:max-w-5xl">
           <DialogHeader>
             <DialogTitle>{vehicle ? t('editTitle') : t('addTitle')}</DialogTitle>
             <DocsLink href="/docs/features/vehicles" variant="hint" className="self-start" />
-            <DialogDescription className="sr-only">
-              {vehicle ? t('editTitle') : t('addTitle')}
+            {/* Which vehicle is open, so the title line answers it before the fields do */}
+            <DialogDescription className={vehicleIdentity ? undefined : 'sr-only'}>
+              {vehicleIdentity ?? (vehicle ? t('editTitle') : t('addTitle'))}
             </DialogDescription>
           </DialogHeader>
 
-          <form
-            ref={formRef}
-            onSubmit={handleSubmit}
-            className="grid gap-x-6 gap-y-4 md:grid-cols-2"
-          >
-            {/* Scan the registration document and fill the form from it */}
-            <div className="md:col-span-2">
-              <ScanDocumentButton onScanned={applyScan} />
-            </div>
-
-            {/* Left: what the vehicle belongs to and where its data comes from */}
-            <div className="space-y-4">
-              <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
-                {t('sectionAssignment')}
-              </p>
-
-              {/* Image upload */}
-              <div className="space-y-2">
-                <Label>{t('photo')}</Label>
-                <div
-                  {...interactiveRow(() => fileRef.current?.click())}
-                  className="group relative flex h-44 cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 border-dashed border-border bg-muted/30 transition-colors hover:border-primary/50 hover:bg-muted/50"
-                >
-                  {preview ? (
-                    <>
-                      <Image
-                        src={preview}
-                        alt="Vehicle preview"
-                        fill
-                        unoptimized
-                        className="object-cover"
-                      />
-                      <div className="absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/20" />
-                      <button
-                        type="button"
-                        onClick={(e) => {
-                          e.stopPropagation()
-                          clearImage()
-                        }}
-                        className="absolute top-2 right-2 flex h-7 w-7 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity group-hover:opacity-100"
-                      >
-                        <X className="h-4 w-4" />
-                      </button>
-                    </>
-                  ) : (
-                    <div className="flex flex-col items-center gap-2 text-muted-foreground">
-                      <Camera className="h-8 w-8" />
-                      <span className="text-sm">{t('clickToUpload')}</span>
-                      <span className="text-xs">{t('imageFormats')}</span>
-                    </div>
-                  )}
-                </div>
-                <input
-                  ref={fileRef}
-                  type="file"
-                  accept="image/jpeg,image/png,image/webp,image/avif"
-                  onChange={handleImageSelect}
-                  className="hidden"
-                />
+          <form ref={formRef} onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
+            <DialogBody className="grid content-start gap-x-8 gap-y-4 md:grid-cols-2">
+              {/* Scan the registration document and fill the form from it */}
+              <div className="md:col-span-2">
+                <ScanDocumentButton onScanned={applyScan} />
               </div>
 
-              {/* Customer selector */}
-              <div className="space-y-2">
-                <Label>{t('customer')}</Label>
-                <CustomerCombobox
-                  value={selectedCustomerId === 'none' ? '' : selectedCustomerId}
-                  initialCustomer={selectedCustomer}
-                  // A customer given by id alone is named once the lookup below answers
-                  placeholder={
-                    selectedCustomerId !== 'none' && !selectedCustomer ? '…' : t('noCustomer')
-                  }
-                  searchPlaceholder={t('searchCustomers')}
-                  noneLabel={t('noCustomer')}
-                  emptyLabel={t('noCustomerFound')}
-                  onChange={(id, customer) => {
-                    setSelectedCustomerId(id || 'none')
-                    setSelectedCustomer(customer)
-                  }}
-                  footer={(close) => (
-                    <div className="border-t p-1">
-                      <button
-                        type="button"
-                        className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
-                        onClick={() => {
-                          close()
-                          setShowCustomerForm(true)
-                        }}
-                      >
-                        <Plus className="h-4 w-4" />
-                        {t('addCustomer')}
-                      </button>
-                    </div>
-                  )}
-                />
-
-                {/* Keeper read off the papers who matches no customer yet */}
-                {scannedOwner?.name && selectedCustomerId === 'none' && (
-                  <div className="space-y-2 rounded-lg border border-dashed border-border bg-muted/30 p-3">
-                    <label className="flex cursor-pointer items-start gap-3">
-                      <Checkbox
-                        className="mt-0.5"
-                        checked={addOwner}
-                        onCheckedChange={(next) => setAddOwner(next === true)}
-                      />
-                      <span className="min-w-0">
-                        <span className="block text-sm">
-                          {t('addScannedOwner', { name: scannedOwner.name })}
-                        </span>
-                        {scannedOwner.address && (
-                          <span className="block truncate text-xs text-muted-foreground">
-                            {scannedOwner.address}
-                          </span>
-                        )}
-                      </span>
-                    </label>
-
-                    {ownerMatches.length > 0 && (
-                      <Button
-                        type="button"
-                        size="sm"
-                        variant="outline"
-                        className="w-full"
-                        onClick={() => setShowOwnerMatch(true)}
-                      >
-                        {t('ownerMatchChoose')}
-                      </Button>
-                    )}
-                  </div>
-                )}
-              </div>
-            </div>
+              {/* Left: what the vehicle belongs to and where its data comes from */}
+              <div className="space-y-4">
+                <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
+                  {t('sectionAssignment')}
+                </p>
 
-            {/* Right: the vehicle's own details */}
-            <div className="space-y-4">
-              <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
-                {t('sectionDetails')}
-              </p>
-
-              <div className="grid grid-cols-2 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor="make">{isMarine ? t('makeMarine') : t('make')}</Label>
-                  <Input
-                    id="make"
-                    name="make"
-                    placeholder={isMarine ? 'Boston Whaler' : 'Toyota'}
-                    defaultValue={vehicle?.make}
-                    required
-                  />
-                </div>
+                {/* Image upload */}
                 <div className="space-y-2">
-                  <Label htmlFor="model">{t('model')}</Label>
-                  <Input
-                    id="model"
-                    name="model"
-                    placeholder={isMarine ? 'Montauk 170' : 'Camry'}
-                    defaultValue={vehicle?.model}
-                    required
+                  <Label>{t('photo')}</Label>
+                  <div
+                    {...interactiveRow(() => fileRef.current?.click())}
+                    className="group relative flex h-44 cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 border-dashed border-border bg-muted/30 transition-colors hover:border-primary/50 hover:bg-muted/50"
+                  >
+                    {preview ? (
+                      <>
+                        <Image
+                          src={preview}
+                          alt="Vehicle preview"
+                          fill
+                          unoptimized
+                          className="object-cover"
+                        />
+                        <div className="absolute inset-0 bg-black/0 transition-colors group-hover:bg-black/20" />
+                        <button
+                          type="button"
+                          onClick={(e) => {
+                            e.stopPropagation()
+                            clearImage()
+                          }}
+                          className="absolute top-2 right-2 flex h-7 w-7 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity group-hover:opacity-100"
+                        >
+                          <X className="h-4 w-4" />
+                        </button>
+                      </>
+                    ) : (
+                      <div className="flex flex-col items-center gap-2 text-muted-foreground">
+                        <Camera className="h-8 w-8" />
+                        <span className="text-sm">{t('clickToUpload')}</span>
+                        <span className="text-xs">{t('imageFormats')}</span>
+                      </div>
+                    )}
+                  </div>
+                  <input
+                    ref={fileRef}
+                    type="file"
+                    accept="image/jpeg,image/png,image/webp,image/avif"
+                    onChange={handleImageSelect}
+                    className="hidden"
                   />
                 </div>
-              </div>
 
-              <div className="grid grid-cols-2 gap-4">
+                {/* Customer selector */}
                 <div className="space-y-2">
-                  <Label htmlFor="year">{t('year')}</Label>
-                  <Input
-                    id="year"
-                    name="year"
-                    type="number"
-                    placeholder="2024"
-                    defaultValue={vehicle?.year}
-                    required
-                  />
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor="mileage">{isMarine ? t('mileageMarine') : t('mileage')}</Label>
-                  <Input
-                    id="mileage"
-                    name="mileage"
-                    type="number"
-                    placeholder="0"
-                    defaultValue={vehicle?.mileage}
+                  <Label>{t('customer')}</Label>
+                  <CustomerCombobox
+                    value={selectedCustomerId === 'none' ? '' : selectedCustomerId}
+                    initialCustomer={selectedCustomer}
+                    // A customer given by id alone is named once the lookup below answers
+                    placeholder={
+                      selectedCustomerId !== 'none' && !selectedCustomer ? '…' : t('noCustomer')
+                    }
+                    searchPlaceholder={t('searchCustomers')}
+                    noneLabel={t('noCustomer')}
+                    emptyLabel={t('noCustomerFound')}
+                    onChange={(id, customer) => {
+                      setSelectedCustomerId(id || 'none')
+                      setSelectedCustomer(customer)
+                    }}
+                    footer={(close) => (
+                      <div className="border-t p-1">
+                        <button
+                          type="button"
+                          className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
+                          onClick={() => {
+                            close()
+                            setShowCustomerForm(true)
+                          }}
+                        >
+                          <Plus className="h-4 w-4" />
+                          {t('addCustomer')}
+                        </button>
+                      </div>
+                    )}
                   />
+
+                  {/* Keeper read off the papers who matches no customer yet */}
+                  {scannedOwner?.name && selectedCustomerId === 'none' && (
+                    <div className="space-y-2 rounded-lg border border-dashed border-border bg-muted/30 p-3">
+                      <label className="flex cursor-pointer items-start gap-3">
+                        <Checkbox
+                          className="mt-0.5"
+                          checked={addOwner}
+                          onCheckedChange={(next) => setAddOwner(next === true)}
+                        />
+                        <span className="min-w-0">
+                          <span className="block text-sm">
+                            {t('addScannedOwner', { name: scannedOwner.name })}
+                          </span>
+                          {scannedOwner.address && (
+                            <span className="block truncate text-xs text-muted-foreground">
+                              {scannedOwner.address}
+                            </span>
+                          )}
+                        </span>
+                      </label>
+
+                      {ownerMatches.length > 0 && (
+                        <Button
+                          type="button"
+                          size="sm"
+                          variant="outline"
+                          className="w-full"
+                          onClick={() => setShowOwnerMatch(true)}
+                        >
+                          {t('ownerMatchChoose')}
+                        </Button>
+                      )}
+                    </div>
+                  )}
                 </div>
               </div>
 
-              {/* Plate and VIN a row each: side by side, with a lookup button in each,
-                  the VIN was too narrow to show all 17 characters. */}
-              <div className="grid gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor="licensePlate">
-                    {isMarine ? t('licensePlateMarine') : t('licensePlate')}
-                  </Label>
-                  <div className="flex gap-2">
+              {/* Right: the vehicle's own details */}
+              <div className="space-y-4">
+                <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
+                  {t('sectionDetails')}
+                </p>
+
+                <div className="grid grid-cols-2 gap-4">
+                  <div className="space-y-2">
+                    <Label htmlFor="make">{isMarine ? t('makeMarine') : t('make')}</Label>
                     <Input
-                      id="licensePlate"
-                      name="licensePlate"
-                      placeholder="ABC-1234"
-                      defaultValue={vehicle?.licensePlate ?? ''}
-                      className="flex-1"
+                      id="make"
+                      name="make"
+                      placeholder={isMarine ? 'Boston Whaler' : 'Toyota'}
+                      defaultValue={vehicle?.make}
+                      required
+                    />
+                  </div>
+                  <div className="space-y-2">
+                    <Label htmlFor="model">{t('model')}</Label>
+                    <Input
+                      id="model"
+                      name="model"
+                      placeholder={isMarine ? 'Montauk 170' : 'Camry'}
+                      defaultValue={vehicle?.model}
+                      required
                     />
-                    {!isMarine && (
-                      <VehicleLookupButton
-                        by="plate"
-                        getValue={() => fieldValue('licensePlate')}
-                        onFound={(data, asked) => applyLookup(data, { by: 'plate', value: asked })}
-                        vehicleId={vehicle?.id}
-                      />
-                    )}
                   </div>
                 </div>
-                <div className="space-y-2">
-                  <Label htmlFor="vin">{isMarine ? t('vinMarine') : t('vin')}</Label>
-                  <div className="flex gap-2">
+
+                <div className="grid grid-cols-2 gap-4">
+                  <div className="space-y-2">
+                    <Label htmlFor="year">{t('year')}</Label>
                     <Input
-                      id="vin"
-                      name="vin"
-                      placeholder="1HGCM82633A004352"
-                      defaultValue={vehicle?.vin ?? ''}
-                      className="flex-1"
+                      id="year"
+                      name="year"
+                      type="number"
+                      placeholder="2024"
+                      defaultValue={vehicle?.year}
+                      required
                     />
-                    {!isMarine && (
-                      <VehicleLookupButton
-                        by="vin"
-                        getValue={() => fieldValue('vin')}
-                        onFound={(data, asked) => applyLookup(data, { by: 'vin', value: asked })}
-                        vehicleId={vehicle?.id}
+                  </div>
+                  <div className="space-y-2">
+                    <Label htmlFor="mileage">{isMarine ? t('mileageMarine') : t('mileage')}</Label>
+                    <Input
+                      id="mileage"
+                      name="mileage"
+                      type="number"
+                      placeholder="0"
+                      defaultValue={vehicle?.mileage}
+                    />
+                  </div>
+                </div>
+
+                {/* Plate and VIN a row each: side by side, with a lookup button in each,
+                  the VIN was too narrow to show all 17 characters. */}
+                <div className="grid gap-4">
+                  <div className="space-y-2">
+                    <Label htmlFor="licensePlate">
+                      {isMarine ? t('licensePlateMarine') : t('licensePlate')}
+                    </Label>
+                    <div className="flex gap-2">
+                      <Input
+                        id="licensePlate"
+                        name="licensePlate"
+                        placeholder="ABC-1234"
+                        defaultValue={vehicle?.licensePlate ?? ''}
+                        className="flex-1"
                       />
-                    )}
+                      {!isMarine && (
+                        <VehicleLookupButton
+                          by="plate"
+                          getValue={() => fieldValue('licensePlate')}
+                          onFound={(data, asked) =>
+                            applyLookup(data, { by: 'plate', value: asked })
+                          }
+                          vehicleId={vehicle?.id}
+                        />
+                      )}
+                    </div>
                   </div>
+                  <div className="space-y-2">
+                    <Label htmlFor="vin">{isMarine ? t('vinMarine') : t('vin')}</Label>
+                    <div className="flex gap-2">
+                      <Input
+                        id="vin"
+                        name="vin"
+                        placeholder="1HGCM82633A004352"
+                        defaultValue={vehicle?.vin ?? ''}
+                        className="flex-1"
+                      />
+                      {!isMarine && (
+                        <VehicleLookupButton
+                          by="vin"
+                          getValue={() => fieldValue('vin')}
+                          onFound={(data, asked) => applyLookup(data, { by: 'vin', value: asked })}
+                          vehicleId={vehicle?.id}
+                        />
+                      )}
+                    </div>
+                  </div>
+                  {/* Under both fields, since either button may have answered */}
+                  {lookupNote && (
+                    <p className="-mt-2 text-xs text-muted-foreground">
+                      {lookupNote.inspectionDue
+                        ? t('lookupInspectionDue', {
+                            source: lookupNote.source,
+                            date: format.dateTime(new Date(lookupNote.inspectionDue), {
+                              dateStyle: 'medium',
+                            }),
+                          })
+                        : t('lookupSource', { source: lookupNote.source })}
+                    </p>
+                  )}
                 </div>
-                {/* Under both fields, since either button may have answered */}
-                {lookupNote && (
-                  <p className="-mt-2 text-xs text-muted-foreground">
-                    {lookupNote.inspectionDue
-                      ? t('lookupInspectionDue', {
-                          source: lookupNote.source,
-                          date: format.dateTime(new Date(lookupNote.inspectionDue), {
-                            dateStyle: 'medium',
-                          }),
-                        })
-                      : t('lookupSource', { source: lookupNote.source })}
-                  </p>
-                )}
-              </div>
 
-              <div className="grid grid-cols-2 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor="color">{t('color')}</Label>
-                  <Input
-                    id="color"
-                    name="color"
-                    placeholder="Silver"
-                    defaultValue={vehicle?.color ?? ''}
-                  />
+                <div className="grid grid-cols-2 gap-4">
+                  <div className="space-y-2">
+                    <Label htmlFor="color">{t('color')}</Label>
+                    <Input
+                      id="color"
+                      name="color"
+                      placeholder="Silver"
+                      defaultValue={vehicle?.color ?? ''}
+                    />
+                  </div>
+                  <div className="space-y-2">
+                    <Label htmlFor="fuelType">{t('fuelType')}</Label>
+                    <Select name="fuelType" value={fuelType} onValueChange={setFuelType}>
+                      <SelectTrigger>
+                        <SelectValue />
+                      </SelectTrigger>
+                      <SelectContent>
+                        <SelectItem value="gasoline">{t('gasoline')}</SelectItem>
+                        <SelectItem value="diesel">{t('diesel')}</SelectItem>
+                        {!isMarine && (
+                          <>
+                            <SelectItem value="electric">{t('electric')}</SelectItem>
+                            <SelectItem value="hybrid">{t('hybrid')}</SelectItem>
+                          </>
+                        )}
+                        {isMarine && <SelectItem value="two-stroke">{t('twoStroke')}</SelectItem>}
+                        <SelectItem value="other">{t('other')}</SelectItem>
+                      </SelectContent>
+                    </Select>
+                  </div>
                 </div>
+
                 <div className="space-y-2">
-                  <Label htmlFor="fuelType">{t('fuelType')}</Label>
-                  <Select name="fuelType" value={fuelType} onValueChange={setFuelType}>
+                  <Label htmlFor="transmission">
+                    {isMarine ? t('transmissionMarine') : t('transmission')}
+                  </Label>
+                  <Select name="transmission" value={transmission} onValueChange={setTransmission}>
                     <SelectTrigger>
                       <SelectValue />
                     </SelectTrigger>
                     <SelectContent>
-                      <SelectItem value="gasoline">{t('gasoline')}</SelectItem>
-                      <SelectItem value="diesel">{t('diesel')}</SelectItem>
-                      {!isMarine && (
+                      {isMarine ? (
                         <>
-                          <SelectItem value="electric">{t('electric')}</SelectItem>
-                          <SelectItem value="hybrid">{t('hybrid')}</SelectItem>
+                          <SelectItem value="outboard">{t('outboard')}</SelectItem>
+                          <SelectItem value="inboard">{t('inboard')}</SelectItem>
+                        </>
+                      ) : (
+                        <>
+                          <SelectItem value="automatic">{t('automatic')}</SelectItem>
+                          <SelectItem value="manual">{t('manual')}</SelectItem>
+                          <SelectItem value="cvt">{t('cvt')}</SelectItem>
                         </>
                       )}
-                      {isMarine && <SelectItem value="two-stroke">{t('twoStroke')}</SelectItem>}
-                      <SelectItem value="other">{t('other')}</SelectItem>
                     </SelectContent>
                   </Select>
                 </div>
-              </div>
 
-              <div className="space-y-2">
-                <Label htmlFor="transmission">
-                  {isMarine ? t('transmissionMarine') : t('transmission')}
-                </Label>
-                <Select name="transmission" value={transmission} onValueChange={setTransmission}>
-                  <SelectTrigger>
-                    <SelectValue />
-                  </SelectTrigger>
-                  <SelectContent>
+                <div className="grid grid-cols-2 gap-4">
+                  <div className="space-y-2">
+                    <Label htmlFor="engineSize">
+                      {isMarine ? t('engineSizeMarine') : t('engineSize')}
+                    </Label>
                     {isMarine ? (
-                      <>
-                        <SelectItem value="outboard">{t('outboard')}</SelectItem>
-                        <SelectItem value="inboard">{t('inboard')}</SelectItem>
-                      </>
+                      <Input
+                        id="engineSize"
+                        name="engineSize"
+                        placeholder="Mercury F 350 XXL Verado V-10 (350 hp)"
+                        defaultValue={vehicle?.engineSize ?? ''}
+                      />
                     ) : (
-                      <>
-                        <SelectItem value="automatic">{t('automatic')}</SelectItem>
-                        <SelectItem value="manual">{t('manual')}</SelectItem>
-                        <SelectItem value="cvt">{t('cvt')}</SelectItem>
-                      </>
+                      <EngineSizeInput
+                        key={vehicle?.id ?? 'new'}
+                        defaultValue={vehicle?.engineSize ?? ''}
+                      />
                     )}
-                  </SelectContent>
-                </Select>
-              </div>
-
-              <div className="grid grid-cols-2 gap-4">
-                <div className="space-y-2">
-                  <Label htmlFor="engineSize">
-                    {isMarine ? t('engineSizeMarine') : t('engineSize')}
-                  </Label>
-                  {isMarine ? (
+                  </div>
+                  <div className="space-y-2">
+                    <Label htmlFor="engineCode">{t('engineCode')}</Label>
                     <Input
-                      id="engineSize"
-                      name="engineSize"
-                      placeholder="Mercury F 350 XXL Verado V-10 (350 hp)"
-                      defaultValue={vehicle?.engineSize ?? ''}
-                    />
-                  ) : (
-                    <EngineSizeInput
-                      key={vehicle?.id ?? 'new'}
-                      defaultValue={vehicle?.engineSize ?? ''}
+                      id="engineCode"
+                      name="engineCode"
+                      placeholder="2AR-FE"
+                      defaultValue={vehicle?.engineCode ?? ''}
                     />
-                  )}
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor="engineCode">{t('engineCode')}</Label>
-                  <Input
-                    id="engineCode"
-                    name="engineCode"
-                    placeholder="2AR-FE"
-                    defaultValue={vehicle?.engineCode ?? ''}
-                  />
+                  </div>
                 </div>
-              </div>
 
-              {showTypeKey && (
-                <div className="space-y-2">
-                  <div className="grid grid-cols-2 gap-4">
-                    <div className="space-y-2">
-                      <Label htmlFor="hsn">{t('hsn')}</Label>
-                      <Input
-                        id="hsn"
-                        name="hsn"
-                        placeholder="0603"
-                        inputMode="numeric"
-                        maxLength={6}
-                        autoComplete="off"
-                        className="font-mono"
-                        defaultValue={vehicle?.hsn ?? ''}
-                      />
-                    </div>
-                    <div className="space-y-2">
-                      <Label htmlFor="tsn">{t('tsn')}</Label>
-                      <Input
-                        id="tsn"
-                        name="tsn"
-                        placeholder="BFQ"
-                        maxLength={12}
-                        autoComplete="off"
-                        className="font-mono uppercase"
-                        defaultValue={vehicle?.tsn ?? ''}
-                      />
+                {showTypeKey && (
+                  <div className="space-y-2">
+                    <div className="grid grid-cols-2 gap-4">
+                      <div className="space-y-2">
+                        <Label htmlFor="hsn">{t('hsn')}</Label>
+                        <Input
+                          id="hsn"
+                          name="hsn"
+                          placeholder="0603"
+                          inputMode="numeric"
+                          maxLength={6}
+                          autoComplete="off"
+                          className="font-mono"
+                          defaultValue={vehicle?.hsn ?? ''}
+                        />
+                      </div>
+                      <div className="space-y-2">
+                        <Label htmlFor="tsn">{t('tsn')}</Label>
+                        <Input
+                          id="tsn"
+                          name="tsn"
+                          placeholder="BFQ"
+                          maxLength={12}
+                          autoComplete="off"
+                          className="font-mono uppercase"
+                          defaultValue={vehicle?.tsn ?? ''}
+                        />
+                      </div>
                     </div>
+                    <p className="text-xs text-muted-foreground">{t('typeKeyHint')}</p>
                   </div>
-                  <p className="text-xs text-muted-foreground">{t('typeKeyHint')}</p>
-                </div>
-              )}
+                )}
 
-              {!isMarine && (
-                <div className="space-y-2">
-                  <Label htmlFor="inspectionDueAt">{t('inspectionDue')}</Label>
-                  <Input
-                    id="inspectionDueAt"
-                    name="inspectionDueAt"
-                    type="date"
-                    defaultValue={openedInspectionDue}
-                  />
-                  <p className="text-xs text-muted-foreground">
-                    {vehicle?.inspectionStatus?.source &&
-                    vehicle.inspectionStatus.source !== 'manual'
-                      ? t('inspectionDueFromRegistry')
-                      : t('inspectionDueHint')}
-                  </p>
-                </div>
-              )}
-            </div>
+                {!isMarine && (
+                  <div className="space-y-2">
+                    <Label htmlFor="inspectionDueAt">{t('inspectionDue')}</Label>
+                    <Input
+                      id="inspectionDueAt"
+                      name="inspectionDueAt"
+                      type="date"
+                      defaultValue={openedInspectionDue}
+                    />
+                    <p className="text-xs text-muted-foreground">
+                      {vehicle?.inspectionStatus?.source &&
+                      vehicle.inspectionStatus.source !== 'manual'
+                        ? t('inspectionDueFromRegistry')
+                        : t('inspectionDueHint')}
+                    </p>
+                  </div>
+                )}
+              </div>
+            </DialogBody>
 
-            <div className="flex justify-end gap-3 border-t pt-4 md:col-span-2">
+            <DialogFooter>
               <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
                 {tc('cancel')}
               </Button>
@@ -886,7 +896,7 @@ export function VehicleForm({
                 {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                 {vehicle ? tc('saveChanges') : t('addTitle')}
               </Button>
-            </div>
+            </DialogFooter>
           </form>
         </DialogContent>
       </Dialog>