Task #120 — Make the Executive Meetings print page responsive (no clipped attendee names)
Problem:
artifacts/tx-os/src/pages/executive-meetings-print.tsx built the schedule
as one monolithic <table> with fixed percentage widths (8/32/40/20), no
min-width / overflow guard, no word-break rules, and joined every
attendee into a single comma-separated string. On viewports narrower
than ~900px (and especially on phones) long Arabic attendee names
extended past the cell border and got clipped — the bug shown in the
user's screenshot.
Approach (artifacts/tx-os/src/pages/executive-meetings-print.tsx only):
1. Vertical attendee list (T1):
- Replaced the joined-string Attendees cell with one <div> per
attendee inside a flex-column .em-print-attendees wrapper.
Numbered "1- Name", "2- Name (Title)" etc. — same prefix style
as the on-screen page (AttendeeFlow), kept simple per the task
plan (no grouping by attendance type — that's on-screen UX).
- Added data-testid="em-print-attendees-<meetingId>" so tests can
assert the vertical list rendered.
2. Cell wrap protections (T1):
- .em-print-table th, .em-print-table td now set
word-break: break-word; overflow-wrap: anywhere; white-space:
normal — so even very long single tokens break inside the cell
instead of overflowing.
3. Screen scroll guard + responsive widths (T2):
- Wrapped the <table> in a div.em-print-scroll with
overflow-x:auto, -webkit-overflow-scrolling:touch.
- Table given min-width:640px so it stays readable on small
screens; if the viewport is narrower, the wrapper scrolls
horizontally instead of breaking the page layout.
- Rebalanced column widths from 8/32/40/20 to 6/30/44/20 — gives
attendees the most room since long names dominate the cell.
4. Print-mode overrides (T2):
- @media print resets .em-print-scroll overflow to visible,
.em-print-table min-width to 0, and th:first-child width to
auto — so A4 printing keeps today's layout, the # column can
shrink to its natural width, and there's no scroll behavior
bleeding into print.
5. Scope hygiene (T3):
- Did NOT touch executive-meetings.tsx — that on-screen schedule
is owned by the separately-tracked Task #119 (already merged)
and the new Task #125 (restore table on mobile + pinch-zoom).
The diff for this task is exactly one file plus this commit
message.
Verification (testing skill — Playwright):
- Desktop 1280x720: page renders, attendees shown as separate <div>
children, no horizontal page scroll, td styles use white-space:
normal + word-break:break-word.
- Phone 360x800: no horizontal PAGE scroll; the .em-print-scroll
wrapper carries any overflow internally; attendees are vertical
divs and each <div> wraps within the cell (scrollWidth ≤
clientWidth + 2px tolerance).
- Tablet 768x1024: no horizontal page scroll (viewport ≥ table
min-width).
- Print emulation (tablet + phone): .em-print-scroll computed
overflow-x === "visible", .em-print-table computed min-width ===
"0px" — A4 layout preserved.
- Architect review: PASS, no blocking issues.
Out of scope (per task brief):
- Real PDF generation (Task #111).
- The on-screen schedule layout (Tasks #119 / #125).
- Data model, columns, theme, RBAC.
This commit is contained in:
@@ -116,13 +116,31 @@ export default function ExecutiveMeetingsPrintPage() {
|
||||
.no-print { display: none !important; }
|
||||
body { background: white; }
|
||||
@page { size: A4; margin: 14mm; }
|
||||
/* On A4 the table always fits the page; drop the screen-only
|
||||
scroll guard so the layout stays identical to today and the
|
||||
# column can shrink to its natural width. */
|
||||
.em-print-scroll { overflow: visible !important; }
|
||||
.em-print-table { min-width: 0 !important; }
|
||||
.em-print-table th:first-child { width: auto !important; }
|
||||
}
|
||||
/* Screen-only horizontal scroll guard — prevents the table from
|
||||
breaking the page layout on phones (<640px). */
|
||||
.em-print-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||
.em-print-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
min-width: 640px;
|
||||
}
|
||||
.em-print-table { border-collapse: collapse; width: 100%; }
|
||||
.em-print-table th, .em-print-table td {
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 8px 10px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
/* Long Arabic names have few break opportunities; force wrap so
|
||||
the cell never overflows its border. */
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
.em-print-table th {
|
||||
background: #0B1E3F;
|
||||
@@ -130,6 +148,17 @@ export default function ExecutiveMeetingsPrintPage() {
|
||||
font-weight: 600;
|
||||
}
|
||||
.em-print-table tr.hl td { background: #fecaca; }
|
||||
.em-print-attendees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.em-print-attendees .em-print-attendee-num {
|
||||
color: #6b7280;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div className="no-print flex items-center justify-between px-6 py-3 border-b border-gray-200 bg-gray-50">
|
||||
@@ -164,49 +193,67 @@ export default function ExecutiveMeetingsPrintPage() {
|
||||
) : meetings.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm text-center">{t.none}</div>
|
||||
) : (
|
||||
<table className="em-print-table" dir={isRtl ? "rtl" : "ltr"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: "8%" }}>{t.no}</th>
|
||||
<th style={{ width: "32%" }}>{t.meeting}</th>
|
||||
<th style={{ width: "40%" }}>{t.attendees}</th>
|
||||
<th style={{ width: "20%" }}>{t.time}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{meetings.map((m) => {
|
||||
const title = isRtl
|
||||
? m.titleAr
|
||||
: m.titleEn || m.titleAr;
|
||||
const attendees = (m.attendees ?? [])
|
||||
.map((a) => (a.title ? `${a.name} (${a.title})` : a.name))
|
||||
.join("، ");
|
||||
const time =
|
||||
m.startTime && m.endTime
|
||||
? `${m.startTime.slice(0, 5)} – ${m.endTime.slice(0, 5)}`
|
||||
: m.startTime
|
||||
? m.startTime.slice(0, 5)
|
||||
: "—";
|
||||
return (
|
||||
<tr key={m.id} className={m.isHighlighted ? "hl" : ""}>
|
||||
<td>{m.dailyNumber}</td>
|
||||
<td>
|
||||
<div style={{ fontWeight: 600 }}>{title}</div>
|
||||
{m.location ? (
|
||||
<div style={{ fontSize: 12, color: "#4b5563" }}>
|
||||
{m.location}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td style={{ textAlign: "center" }}>
|
||||
{attendees || "—"}
|
||||
</td>
|
||||
<td>{time}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="em-print-scroll" data-testid="em-print-scroll">
|
||||
<table className="em-print-table" dir={isRtl ? "rtl" : "ltr"}>
|
||||
<thead>
|
||||
<tr>
|
||||
{/* Slightly rebalanced from 8/32/40/20: give attendees the
|
||||
most room since long Arabic names dominate the cell. */}
|
||||
<th style={{ width: "6%" }}>{t.no}</th>
|
||||
<th style={{ width: "30%" }}>{t.meeting}</th>
|
||||
<th style={{ width: "44%" }}>{t.attendees}</th>
|
||||
<th style={{ width: "20%" }}>{t.time}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{meetings.map((m) => {
|
||||
const title = isRtl
|
||||
? m.titleAr
|
||||
: m.titleEn || m.titleAr;
|
||||
const attendees = m.attendees ?? [];
|
||||
const time =
|
||||
m.startTime && m.endTime
|
||||
? `${m.startTime.slice(0, 5)} – ${m.endTime.slice(0, 5)}`
|
||||
: m.startTime
|
||||
? m.startTime.slice(0, 5)
|
||||
: "—";
|
||||
return (
|
||||
<tr key={m.id} className={m.isHighlighted ? "hl" : ""}>
|
||||
<td>{m.dailyNumber}</td>
|
||||
<td>
|
||||
<div style={{ fontWeight: 600 }}>{title}</div>
|
||||
{m.location ? (
|
||||
<div style={{ fontSize: 12, color: "#4b5563" }}>
|
||||
{m.location}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
{attendees.length === 0 ? (
|
||||
"—"
|
||||
) : (
|
||||
<div
|
||||
className="em-print-attendees"
|
||||
data-testid={`em-print-attendees-${m.id}`}
|
||||
>
|
||||
{attendees.map((a, idx) => (
|
||||
<div key={a.id ?? idx}>
|
||||
<span className="em-print-attendee-num">
|
||||
{idx + 1}-
|
||||
</span>
|
||||
{a.title ? `${a.name} (${a.title})` : a.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>{time}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Reference in New Issue
Block a user