From 9b617ba243a34d34a7e79a89f5df889ee9c8f139 Mon Sep 17 00:00:00 2001 From: Asapteejo Date: Thu, 17 Sep 2026 00:01:13 +0100 Subject: [PATCH 1/2] feat(mobile): make tables, forms and touch targets work at 375px MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured at 375px with touch emulation, before and after. Tables (the worst offender). DataTable only wrapped the table in overflow-x-auto, so /admin/payments rendered 973px wide inside a 333px column and /admin/invoices 775px — three screens of sideways scrolling. Below `md` each row is now a card: first column as the heading, the rest as label/value pairs, actions preserved. The table returns at `md`. This lands once in the shared component, so all nine DataTable screens plus SimpleDataTable/DataTableCard get it. The Deal Board was worse — a hardcoded min-w-[960px] grid, not a table at all — and now stacks its stage columns below `md`. iOS zoom. Input/Select/Textarea were 14px, and iOS Safari zooms the page whenever a focused field is under 16px — every form on every phone. They are 16px below `sm` and keep 14px above it. Same for the 9 inline input styles that bypassed the primitives, which were also 38px tall (now 44). Phone fields in the front-desk and quick-create forms gained type="tel" so they raise the right keyboard. Touch targets (44px minimum). Button size="sm" was 36px and is the admin row-action size; dialog close 36px; DataTable sort controls 16px tall; pagination 36px; theme toggle 36px; announcement dismiss 28px; quick actions 40px; footer links 20px; property filter checkboxes 13px. All are 44px on phones and keep their denser desktop sizing from `sm` up. Content priority. Admin pages put ~1600px of chrome above the data; the ambient clock is hidden on phones and page descriptions clamp to two lines. /properties buried every listing under six stacked filters, which now collapse behind a toggle (CSS-only, still server-rendered, always open from `md` up). Dialogs. The bottom-sheet behaviour was already right; it gained safe-area padding so actions clear the home indicator, and its footer buttons stack full-width on phones. Measured result (small targets per screen, iOS-zoom fields, side-scroll): /admin 31 -> 11, 960px grid gone /admin/payments 20 -> 6, 973px table gone, 6 zoom fields -> 0 /admin/invoices 26 -> 5, 775px table gone, 6 zoom fields -> 0 tenant / 9 -> 4, 3 zoom fields -> 0 tenant /contact 8 -> 4, 11px horizontal overflow -> 0 Design tokens and dark mode are untouched; this is layout and interaction only. Co-Authored-By: Claude Opus 5 --- src/app/(admin)/admin/contracts/page.tsx | 2 +- src/app/(marketing)/properties/page.tsx | 40 +++++--- src/app/(marketing)/testimonials/page.tsx | 4 +- src/components/admin/add-buyer-form.tsx | 2 +- .../admin/announcement-composer.tsx | 2 +- src/components/admin/branding-management.tsx | 2 +- src/components/admin/deal-board-view.tsx | 7 +- src/components/admin/front-desk-logbook.tsx | 6 +- src/components/admin/invoice-create-form.tsx | 2 +- src/components/admin/users-management.tsx | 2 +- src/components/dashboard/quick-actions.tsx | 2 +- .../dashboard/quick-create-forms.tsx | 6 +- src/components/marketing/marketing-footer.tsx | 16 +-- src/components/messaging/message-composer.tsx | 2 +- src/components/messaging/new-thread-form.tsx | 2 +- src/components/portal/announcement-banner.tsx | 2 +- src/components/portal/dashboard-shell.tsx | 14 ++- src/components/shared/theme-toggle.tsx | 2 +- src/components/ui/button.tsx | 5 +- src/components/ui/data-table.tsx | 98 ++++++++++++++++++- src/components/ui/dialog.tsx | 20 +++- src/components/ui/input.tsx | 5 +- src/components/ui/select.tsx | 3 +- src/components/ui/textarea.tsx | 3 +- 24 files changed, 193 insertions(+), 56 deletions(-) diff --git a/src/app/(admin)/admin/contracts/page.tsx b/src/app/(admin)/admin/contracts/page.tsx index c16211d..cc41e25 100644 --- a/src/app/(admin)/admin/contracts/page.tsx +++ b/src/app/(admin)/admin/contracts/page.tsx @@ -109,7 +109,7 @@ function UploadContractForm({ transactions }: { transactions: TransactionWithout name="file" accept=".pdf,application/pdf" required - className="w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-800)] file:mr-3 file:rounded file:border-0 file:bg-[var(--sand-100)] file:px-2 file:py-0.5 file:text-xs file:font-medium" + className="w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-800)] file:mr-3 file:rounded file:border-0 file:bg-[var(--sand-100)] file:px-2 file:py-0.5 file:text-xs file:font-medium" />
diff --git a/src/app/(marketing)/properties/page.tsx b/src/app/(marketing)/properties/page.tsx index 207a35b..5a0e473 100644 --- a/src/app/(marketing)/properties/page.tsx +++ b/src/app/(marketing)/properties/page.tsx @@ -62,11 +62,23 @@ export default async function PropertiesPage({
-
+ {/* On a phone these six fields pushed every listing below the fold, + so they collapse behind a toggle and buyers see inventory first. + Checkbox + peer is used rather than
or JS so the panel + works server-rendered, and it is force-open from `md` up. The + checkbox has no name, so it is never submitted with the form. */} + +
-
+ + Tap to {activeFilters.length > 0 ? "edit" : "filter"} + + +
Reserved -
+ {/* Sibling of the peer checkbox so the actions collapse with the + fields they apply to, instead of floating above the listings. */} +
+ + + + +
-
- - - - -
diff --git a/src/app/(marketing)/testimonials/page.tsx b/src/app/(marketing)/testimonials/page.tsx index e951de3..a60d317 100644 --- a/src/app/(marketing)/testimonials/page.tsx +++ b/src/app/(marketing)/testimonials/page.tsx @@ -59,8 +59,8 @@ export default async function TestimonialsPage({ ))} - - + + diff --git a/src/components/admin/add-buyer-form.tsx b/src/components/admin/add-buyer-form.tsx index 7630995..4c3c800 100644 --- a/src/components/admin/add-buyer-form.tsx +++ b/src/components/admin/add-buyer-form.tsx @@ -13,7 +13,7 @@ import type { ProvisionUserResult } from "@/modules/provisioning/provision-user" const INITIAL: ProvisionUserResult | null = null; const inputCls = - "admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]"; + "admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]"; const labelCls = "block text-xs font-medium uppercase tracking-[0.12em] text-[var(--ink-500)]"; diff --git a/src/components/admin/announcement-composer.tsx b/src/components/admin/announcement-composer.tsx index 03d3dfc..e10eb68 100644 --- a/src/components/admin/announcement-composer.tsx +++ b/src/components/admin/announcement-composer.tsx @@ -9,7 +9,7 @@ import { Select } from "@/components/ui/select"; const INITIAL: AnnouncementFormState = { ok: false, error: null }; const inputClass = - "admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]"; + "admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]"; function PostButton() { const { pending } = useFormStatus(); diff --git a/src/components/admin/branding-management.tsx b/src/components/admin/branding-management.tsx index 0fdf9c2..ced51cc 100644 --- a/src/components/admin/branding-management.tsx +++ b/src/components/admin/branding-management.tsx @@ -682,7 +682,7 @@ function ColorField({
onChange(event.target.value.toUpperCase())} /> - onChange(event.target.value.toUpperCase())} /> + onChange(event.target.value.toUpperCase())} />
); diff --git a/src/components/admin/deal-board-view.tsx b/src/components/admin/deal-board-view.tsx index a4362c0..f8a8fef 100644 --- a/src/components/admin/deal-board-view.tsx +++ b/src/components/admin/deal-board-view.tsx @@ -514,8 +514,11 @@ export function DealBoardView({ ) : null}
-
-
+ {/* The 960px floor is what keeps the stage columns side by side, but on + a phone it forced the whole board to scroll sideways inside a 335px + viewport. Below `md` the stages stack vertically instead. */} +
+
{visibleColumns.map((column) => ( {inspect && column.key === "OVERDUE" ? ( diff --git a/src/components/admin/front-desk-logbook.tsx b/src/components/admin/front-desk-logbook.tsx index 2380515..b7af98b 100644 --- a/src/components/admin/front-desk-logbook.tsx +++ b/src/components/admin/front-desk-logbook.tsx @@ -10,7 +10,7 @@ import { WhatsAppButton } from "@/components/shared/whatsapp-button"; import { Select } from "@/components/ui/select"; const inputClass = - "admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white px-3 py-2 text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]"; + "admin-focus w-full rounded-[var(--radius-md)] border border-[var(--line)] bg-white min-h-11 px-3 py-2 text-base sm:text-sm text-[var(--ink-900)] placeholder:text-[var(--ink-400)]"; const buttonClass = "admin-focus inline-flex items-center justify-center gap-1.5 rounded-[var(--radius-md)] bg-[var(--brand-700)] px-3.5 py-2 text-sm font-semibold text-white transition-colors hover:bg-[var(--brand-800,#15803d)]"; @@ -45,7 +45,7 @@ export function FrontDeskLogbookView({ logbook }: { logbook: FrontDeskLogbook })
- +
@@ -105,7 +105,7 @@ export function FrontDeskLogbookView({ logbook }: { logbook: FrontDeskLogbook }) - + - + @@ -66,7 +66,7 @@ export function LeadQuickForm({ onSuccess }: { onSuccess: () => void }) { - +