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:
riyadhafraa
2026-04-29 09:06:25 +00:00
parent c0cf2115dd
commit ed52b845f1
2 changed files with 2 additions and 150 deletions
+1 -1
View File
@@ -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,