Improve navigation and search responsiveness across different screen sizes

Update mobile layout for audit and manage sections, introduce horizontal scroll affordances with gradient fades for tabs, and adjust search input and heading sizes for better mobile usability.
This commit is contained in:
Riyadh
2026-05-19 11:56:36 +00:00
parent 356ab2c7c8
commit 675a60506d
+161 -80
View File
@@ -49,6 +49,7 @@ import {
} from "lucide-react";
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
import { AppDock } from "@/components/app-dock";
import { useIsMobile } from "@/hooks/use-mobile";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -1142,8 +1143,12 @@ function ExecutiveMeetingsPageInner() {
</div>
</div>
<nav className="border-t border-gray-100 bg-white overflow-x-auto">
<div className="max-w-screen-2xl mx-auto px-2 sm:px-4 flex items-center gap-1 sm:gap-2">
{/* Tabs row. The inner div scrolls horizontally on narrow
phones; the absolute gradients at each edge are a visual
"more tabs →" affordance so users don't think the row is
simply cut off. */}
<nav className="relative border-t border-gray-100 bg-white">
<div className="max-w-screen-2xl mx-auto px-2 sm:px-4 flex items-center gap-1 sm:gap-2 overflow-x-auto scrollbar-thin">
<div className="flex gap-1 sm:gap-2 min-w-max">
{!isPresidentView && SECTIONS.filter(({ key }) => isSectionVisible(key, me)).map(
({ key, icon: Icon }) => {
@@ -1171,6 +1176,17 @@ function ExecutiveMeetingsPageInner() {
<div className="flex-1" />
<div id="em-toolbar-portal" className="flex items-center gap-2 py-1 print:hidden" />
</div>
{/* Edge fade affordances only visible on narrow screens
where the tab row actually scrolls. pointer-events-none
so they never intercept taps on the tab buttons below. */}
<div
className="pointer-events-none absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-white to-transparent sm:hidden"
aria-hidden="true"
/>
<div
className="pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-white to-transparent sm:hidden"
aria-hidden="true"
/>
</nav>
</header>
@@ -6747,17 +6763,16 @@ function ManageSection({
<div
ref={headingRef}
data-testid="em-manage-heading-bar"
className="sticky z-30 bg-[#f4f6fb] -mx-3 sm:-mx-6 px-3 sm:px-6 py-2 shadow-[0_4px_6px_-4px_rgba(11,30,63,0.15)] flex items-center justify-between gap-3 flex-wrap"
className="sticky z-30 bg-[#f4f6fb] -mx-3 sm:-mx-6 px-3 sm:px-6 py-2 shadow-[0_4px_6px_-4px_rgba(11,30,63,0.15)] flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3"
style={{ top: "var(--em-header-h, 0px)" }}
>
<h2 className="text-xl sm:text-2xl font-bold text-[#0B1E3F]">
<h2 className="text-lg sm:text-2xl font-bold text-[#0B1E3F]">
{t("executiveMeetings.manage.heading")}
</h2>
<div className="flex items-center gap-2 flex-wrap">
{/* #332: in-day search box for the Manage tab. Reuses the
same locale strings as the Schedule tab so wording stays
identical across tabs. */}
<div className="relative">
{/* #332: in-day search box. Full-width on mobile so the user
can read what they're typing; sm: shrinks back to inline. */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-2 w-full sm:w-auto">
<div className="relative w-full sm:w-auto">
<Input
type="search"
value={searchQuery}
@@ -6765,7 +6780,7 @@ function ManageSection({
placeholder={t("executiveMeetings.schedule.searchPlaceholder")}
aria-label={t("executiveMeetings.schedule.searchAria")}
data-testid="em-manage-search"
className="h-8 w-44 sm:w-56 text-sm bg-white"
className="h-9 w-full sm:w-56 text-sm bg-white"
/>
{searchQuery && (
<button
@@ -6776,26 +6791,28 @@ function ManageSection({
data-testid="em-manage-search-clear"
className={
"absolute top-1/2 -translate-y-1/2 text-muted-foreground hover:text-[#0B1E3F] " +
(isRtl ? "left-1.5" : "right-1.5")
(isRtl ? "left-2" : "right-2")
}
>
<X className="w-3.5 h-3.5" />
</button>
)}
</div>
<label className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">{t("executiveMeetings.date")}</span>
<input
type="date"
value={date}
onChange={(e) => onDateChange(e.target.value)}
className="border border-gray-300 rounded px-2 py-1 text-sm bg-white"
/>
</label>
<Button onClick={openCreate} className="bg-[#0B1E3F] gap-1" data-testid="em-add-meeting">
<Plus className="w-4 h-4" />
{t("executiveMeetings.manage.addMeeting")}
</Button>
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm flex-1 sm:flex-none">
<span className="text-muted-foreground hidden sm:inline">{t("executiveMeetings.date")}</span>
<input
type="date"
value={date}
onChange={(e) => onDateChange(e.target.value)}
className="border border-gray-300 rounded px-2 py-1.5 text-sm bg-white w-full sm:w-auto"
/>
</label>
<Button onClick={openCreate} className="bg-[#0B1E3F] gap-1 shrink-0" data-testid="em-add-meeting">
<Plus className="w-4 h-4" />
<span>{t("executiveMeetings.manage.addMeeting")}</span>
</Button>
</div>
</div>
</div>
@@ -7942,6 +7959,7 @@ function AuditSection({
isRtl: boolean;
t: (k: string) => string;
}) {
const isMobile = useIsMobile();
const [date, setDate] = useState("");
const [dateTo, setDateTo] = useState("");
const [entityType, setEntityType] = useState("all");
@@ -8053,75 +8071,138 @@ function AuditSection({
</div>
</div>
<div className="bg-white border border-gray-200 rounded-md overflow-hidden">
<table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
<thead className="bg-gray-50 text-[#0B1E3F]">
<tr>
<th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.actor")}</th>
<th className="px-3 py-2 text-start w-44">{t("executiveMeetings.audit.col.when")}</th>
<th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.action")}</th>
<th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.entity")}</th>
<th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.changes")}</th>
</tr>
</thead>
<tbody>
{isLoading && (
{/* Desktop / tablet md: full table. Phone < md: stacked cards
so 5 columns don't get squashed into a 390px viewport. */}
{!isMobile ? (
<div className="bg-white border border-gray-200 rounded-md overflow-hidden">
<table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
<thead className="bg-gray-50 text-[#0B1E3F]">
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
{t("common.loading")}
</td>
<th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.actor")}</th>
<th className="px-3 py-2 text-start w-44">{t("executiveMeetings.audit.col.when")}</th>
<th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.action")}</th>
<th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.entity")}</th>
<th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.changes")}</th>
</tr>
)}
{!isLoading && entries.length === 0 && (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
{t("executiveMeetings.audit.empty")}
</td>
</tr>
)}
{entries.map((e) => {
const actor = e.actor
? isRtl
? e.actor.displayNameAr || e.actor.username
: e.actor.displayNameEn || e.actor.username
: "—";
return (
<tr
key={e.id}
className="border-t border-gray-100 align-top"
data-testid={`em-audit-row-${e.id}`}
>
<td className="px-3 py-2">{actor}</td>
<td className="px-3 py-2 text-xs text-gray-600 whitespace-nowrap">
{new Date(e.performedAt).toLocaleString()}
</thead>
<tbody>
{isLoading && (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
{t("common.loading")}
</td>
<td className="px-3 py-2 text-xs">
</tr>
)}
{!isLoading && entries.length === 0 && (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
{t("executiveMeetings.audit.empty")}
</td>
</tr>
)}
{entries.map((e) => {
const actor = e.actor
? isRtl
? e.actor.displayNameAr || e.actor.username
: e.actor.displayNameEn || e.actor.username
: "—";
return (
<tr
key={e.id}
className="border-t border-gray-100 align-top"
data-testid={`em-audit-row-${e.id}`}
>
<td className="px-3 py-2">{actor}</td>
<td className="px-3 py-2 text-xs text-gray-600 whitespace-nowrap">
{new Date(e.performedAt).toLocaleString()}
</td>
<td className="px-3 py-2 text-xs">
{tWithFallback(
t,
`executiveMeetings.audit.action.${e.action}`,
e.action,
)}
</td>
<td className="px-3 py-2 text-xs">
{tWithFallback(
t,
`executiveMeetings.audit.entity.${e.entityType}`,
e.entityType,
)}
</td>
<td className="px-3 py-2 text-xs">
<AuditDiffSummary
oldValue={e.oldValue}
newValue={e.newValue}
t={t}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
) : (
<div className="space-y-2" dir={isRtl ? "rtl" : "ltr"}>
{isLoading && (
<div className="bg-white border border-gray-200 rounded-md py-8 text-center text-sm text-gray-500">
{t("common.loading")}
</div>
)}
{!isLoading && entries.length === 0 && (
<div className="bg-white border border-gray-200 rounded-md py-8 text-center text-sm text-gray-500">
{t("executiveMeetings.audit.empty")}
</div>
)}
{entries.map((e) => {
const actor = e.actor
? isRtl
? e.actor.displayNameAr || e.actor.username
: e.actor.displayNameEn || e.actor.username
: "—";
return (
<div
key={e.id}
className="bg-white border border-gray-200 rounded-md p-3 space-y-2 shadow-sm"
data-testid={`em-audit-row-${e.id}`}
>
<div className="flex items-start justify-between gap-3">
<div className="font-semibold text-[#0B1E3F] text-sm leading-tight">
{actor}
</div>
<div className="text-[11px] text-gray-500 tabular-nums whitespace-nowrap shrink-0">
{new Date(e.performedAt).toLocaleString()}
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className="inline-flex items-center text-[11px] font-medium bg-[#0B1E3F]/10 text-[#0B1E3F] rounded px-1.5 py-0.5">
{tWithFallback(
t,
`executiveMeetings.audit.action.${e.action}`,
e.action,
)}
</td>
<td className="px-3 py-2 text-xs">
</span>
<span className="inline-flex items-center text-[11px] text-gray-600 bg-gray-100 rounded px-1.5 py-0.5">
{tWithFallback(
t,
`executiveMeetings.audit.entity.${e.entityType}`,
e.entityType,
)}
</td>
<td className="px-3 py-2 text-xs">
<AuditDiffSummary
oldValue={e.oldValue}
newValue={e.newValue}
t={t}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</span>
</div>
<div className="text-xs text-gray-700 border-t border-gray-100 pt-2">
<AuditDiffSummary
oldValue={e.oldValue}
newValue={e.newValue}
t={t}
/>
</div>
</div>
);
})}
</div>
)}
<div className="flex items-center justify-between text-xs text-gray-600">
<span data-testid="em-audit-count">
{t("executiveMeetings.audit.pageInfo")}{" "}