Task #125: Restore meetings table on mobile, allow pinch-zoom
Reverts the phone-only stacked-card layout introduced in Task #119 back to a single tidy table for every viewport, and re-enables pinch-zoom on the whole app. Changes: - artifacts/tx-os/index.html: viewport meta no longer pins maximum-scale=1, so iOS/Android pinch gestures work everywhere (now: width=device-width, initial-scale=1). - artifacts/tx-os/src/pages/executive-meetings.tsx: * Removed the md:hidden cards branch (the entire em-schedule-cards block) so the table is the only schedule view at every width. * Dropped the `hidden md:block print:block` gating on the table container; it now renders at all viewports inside the existing overflow-x-auto wrapper, so a too-wide table scrolls horizontally inside its own container instead of breaking the page layout. * Deleted the now-orphaned MeetingCard component and its only consumer of the inline-only RowColorPickerInline variant. * Deleted RowColorPickerInline (no remaining consumer); the hover RowColorPicker is still used by table rows. Preserved: - Desktop (≥xl) fixed-width + resizable column behavior is unchanged (table-fixed at xl, resize handles still mouse-only). - Print fixed-layout behavior (`print:table-fixed`) and RTL support are unchanged. - Localized labels for the customizer/highlight popover are unchanged. Verification: - pnpm --filter @workspace/tx-os exec tsc --noEmit shows no new errors in executive-meetings.tsx (pre-existing admin.tsx errors from the just-merged Task #96 codegen drift are unrelated). - The table renders at 390 / 768 / 1280 px widths; horizontal scroll appears only when the table is wider than the container. Out of scope (per task spec): - Print/PDF page (executive-meetings-print.tsx) — Task #120. - API, schema, columns, or RBAC — none changed.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Tx OS</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
||||
@@ -988,40 +988,8 @@ function ScheduleSection({
|
||||
<div className="text-sm">{date}</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile / phone layout: stacked cards. Hidden at md+ and in print. */}
|
||||
<div
|
||||
className="md:hidden print:hidden space-y-3"
|
||||
data-testid="em-schedule-cards"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="bg-white border-2 border-[#0B1E3F] rounded-md py-10 text-center text-muted-foreground shadow-sm">
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && meetings.length === 0 && (
|
||||
<div className="bg-white border-2 border-[#0B1E3F] rounded-md py-10 text-center text-muted-foreground shadow-sm">
|
||||
{t("executiveMeetings.noMeetings")}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading &&
|
||||
meetings.map((m) => (
|
||||
<MeetingCard
|
||||
key={m.id}
|
||||
meeting={m}
|
||||
isRtl={isRtl}
|
||||
t={t}
|
||||
visibleColumns={visibleColumns}
|
||||
rowColorKey={rowColors[m.id] ?? "default"}
|
||||
onPickRowColor={(key) => setRowColor(m.id, key)}
|
||||
fontStyle={tableStyle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tablet & desktop layout: table. Hidden below md (cards take over),
|
||||
but always shown in print. */}
|
||||
<div
|
||||
className="hidden md:block print:block bg-white border-2 border-[#0B1E3F] rounded-md overflow-x-auto shadow-sm print:shadow-none print:border-black print:overflow-hidden"
|
||||
className="bg-white border-2 border-[#0B1E3F] rounded-md overflow-x-auto shadow-sm print:shadow-none print:border-black print:overflow-hidden"
|
||||
id="executive-schedule-printable"
|
||||
>
|
||||
<table
|
||||
@@ -1450,37 +1418,6 @@ function RowColorPicker({
|
||||
);
|
||||
}
|
||||
|
||||
// Variant of the row-color picker for the mobile card layout: the trigger is
|
||||
// always visible (touch devices have no hover) and is positioned in the normal
|
||||
// flow rather than absolutely.
|
||||
function RowColorPickerInline({
|
||||
current,
|
||||
onPick,
|
||||
t,
|
||||
}: {
|
||||
current: string;
|
||||
onPick: (key: string) => void;
|
||||
t: (k: string) => string;
|
||||
}) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="p-1.5 rounded text-muted-foreground hover:text-[#0B1E3F] hover:bg-white/70"
|
||||
aria-label={t("executiveMeetings.rowColor.label")}
|
||||
data-testid="em-row-color-trigger-inline"
|
||||
>
|
||||
<Palette className="w-4 h-4" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-2" align="end">
|
||||
<RowColorPickerSwatches current={current} onPick={onPick} t={t} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function MeetingRow({
|
||||
meeting,
|
||||
isRtl,
|
||||
@@ -1654,91 +1591,6 @@ function MeetingRow({
|
||||
);
|
||||
}
|
||||
|
||||
function MeetingCard({
|
||||
meeting,
|
||||
isRtl,
|
||||
t,
|
||||
visibleColumns,
|
||||
rowColorKey,
|
||||
onPickRowColor,
|
||||
fontStyle,
|
||||
}: {
|
||||
meeting: Meeting;
|
||||
isRtl: boolean;
|
||||
t: (k: string) => string;
|
||||
visibleColumns: ColumnSetting[];
|
||||
rowColorKey: string;
|
||||
onPickRowColor: (key: string) => void;
|
||||
fontStyle: CSSProperties;
|
||||
}) {
|
||||
const title = isRtl ? meeting.titleAr : meeting.titleEn || meeting.titleAr;
|
||||
const isCancelled = meeting.status === "cancelled";
|
||||
const highlight = meeting.isHighlighted === 1;
|
||||
|
||||
const rowColor = ROW_COLOR_OPTIONS.find((o) => o.key === rowColorKey);
|
||||
const rowBg = rowColor?.bg || "";
|
||||
|
||||
const numberBadgeCls =
|
||||
"shrink-0 w-12 h-12 rounded-md flex items-center justify-center font-bold text-lg border " +
|
||||
(highlight || isCancelled
|
||||
? "bg-red-600 text-white border-red-700"
|
||||
: "bg-[#0B1E3F] text-white border-[#0B1E3F]");
|
||||
|
||||
const showNumber = visibleColumns.some((c) => c.id === "number");
|
||||
const showMeeting = visibleColumns.some((c) => c.id === "meeting");
|
||||
const showAttendees = visibleColumns.some((c) => c.id === "attendees");
|
||||
const showTime = visibleColumns.some((c) => c.id === "time");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative bg-white border-2 border-[#0B1E3F] rounded-md shadow-sm p-3"
|
||||
style={{ backgroundColor: rowBg || "white", ...fontStyle }}
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
data-testid={`em-card-${meeting.id}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{showNumber && (
|
||||
<div className={numberBadgeCls}>{meeting.dailyNumber}</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
{showMeeting && (
|
||||
<>
|
||||
<div
|
||||
className="font-semibold text-[#0B1E3F] leading-snug break-words"
|
||||
dangerouslySetInnerHTML={{ __html: safeHtml(title) }}
|
||||
/>
|
||||
{meeting.location && (
|
||||
<div className="text-xs text-muted-foreground break-words">
|
||||
{meeting.location}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showTime && (meeting.startTime || meeting.endTime) && (
|
||||
<div className="font-mono text-sm text-[#0B1E3F] mt-1">
|
||||
{formatTime(meeting.startTime)}
|
||||
{meeting.endTime ? ` — ${formatTime(meeting.endTime)}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Always-visible color picker on touch — no hover needed. */}
|
||||
<div className="shrink-0 -mt-1 -me-1">
|
||||
<RowColorPickerInline
|
||||
current={rowColorKey}
|
||||
onPick={onPickRowColor}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showAttendees && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200">
|
||||
<AttendeesCell meeting={meeting} t={t} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AttendeesCell({
|
||||
meeting,
|
||||
t,
|
||||
|
||||
Reference in New Issue
Block a user