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:
@@ -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")}{" "}
|
||||
|
||||
Reference in New Issue
Block a user