Add zoom and fullscreen controls to the meeting schedule table
Add zoom and fullscreen controls to the executive meeting schedule page, including zoom in/out buttons, a fullscreen toggle, and Escape key handling for exiting fullscreen. The heading height calculation has been removed and hardcoded to 0px. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 608173c1-6210-41cf-be7d-3a913963059f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/jwG0a8d Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -43,6 +43,10 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Search,
|
||||
Maximize,
|
||||
Minimize2,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from "lucide-react";
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -1145,6 +1149,14 @@ function ScheduleSection({
|
||||
// matched against title, attendee names/titles, location, notes, and
|
||||
// any merge text on the row. Empty needle → pass-through.
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [tableZoom, setTableZoom] = useState(90);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!isFullscreen) return;
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setIsFullscreen(false); };
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [isFullscreen]);
|
||||
// Reset whenever the active day changes — a search is "scoped to the
|
||||
// currently visible date" per the spec, so jumping days clears it.
|
||||
useEffect(() => {
|
||||
@@ -2351,28 +2363,12 @@ function ScheduleSection({
|
||||
[reorderRows],
|
||||
);
|
||||
|
||||
// #267: Same dynamic-measurement trick as the page header — the title
|
||||
// row wraps on narrow widths (heading + edit toggle + date picker), so
|
||||
// we publish its live height so the table thead can sit flush below.
|
||||
const headingRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const heading = headingRef.current;
|
||||
if (!heading) return;
|
||||
const root = heading.closest<HTMLElement>("[data-em-root]");
|
||||
if (!root) return;
|
||||
const update = () => {
|
||||
root.style.setProperty("--em-heading-h", `${heading.offsetHeight}px`);
|
||||
};
|
||||
update();
|
||||
const ro = new ResizeObserver(update);
|
||||
ro.observe(heading);
|
||||
window.addEventListener("resize", update);
|
||||
const root = document.querySelector<HTMLElement>("[data-em-root]");
|
||||
if (root) root.style.setProperty("--em-heading-h", "0px");
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
window.removeEventListener("resize", update);
|
||||
// Reset so other sections (which don't render this heading) don't
|
||||
// inherit a stale offset for any sticky descendants.
|
||||
root.style.setProperty("--em-heading-h", "0px");
|
||||
if (root) root.style.setProperty("--em-heading-h", "0px");
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -2478,6 +2474,35 @@ function ScheduleSection({
|
||||
clearLabel={t("executiveMeetings.schedule.searchClear")}
|
||||
isRtl={isRtl}
|
||||
/>
|
||||
<div className="flex items-center gap-0.5 border border-gray-300 rounded-md bg-white">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTableZoom((z) => Math.max(50, z - 10))}
|
||||
disabled={tableZoom <= 50}
|
||||
className="inline-flex items-center justify-center w-7 h-7 text-[#0B1E3F] hover:bg-gray-100 rounded-s-md disabled:opacity-30 transition-colors"
|
||||
title={`${tableZoom - 10}%`}
|
||||
>
|
||||
<ZoomOut className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span className="text-[11px] font-medium text-[#0B1E3F] w-9 text-center tabular-nums select-none">{tableZoom}%</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTableZoom((z) => Math.min(150, z + 10))}
|
||||
disabled={tableZoom >= 150}
|
||||
className="inline-flex items-center justify-center w-7 h-7 text-[#0B1E3F] hover:bg-gray-100 rounded-e-md disabled:opacity-30 transition-colors"
|
||||
title={`${tableZoom + 10}%`}
|
||||
>
|
||||
<ZoomIn className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFullscreen((f) => !f)}
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white text-[#0B1E3F] w-8 h-8 hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-[#0B1E3F]/40"
|
||||
title={isFullscreen ? "Exit fullscreen" : "Fullscreen"}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize className="w-4 h-4" />}
|
||||
</button>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<span className="text-muted-foreground">{t("executiveMeetings.date")}</span>
|
||||
<input
|
||||
@@ -2491,14 +2516,26 @@ function ScheduleSection({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 print:space-y-2">
|
||||
<div className={
|
||||
isFullscreen
|
||||
? "fixed inset-0 z-[9999] bg-[#f4f6fb] overflow-auto p-4 print:static print:p-0 print:overflow-visible"
|
||||
: "space-y-4 print:space-y-2"
|
||||
}>
|
||||
{portalTarget && createPortal(toolbarControls, portalTarget)}
|
||||
<div
|
||||
ref={headingRef}
|
||||
data-testid="em-schedule-heading-bar"
|
||||
className="print:hidden"
|
||||
style={{ top: "var(--em-header-h, 0px)" }}
|
||||
/>
|
||||
|
||||
{isFullscreen && (
|
||||
<div className="flex items-center justify-between mb-3 print:hidden">
|
||||
<h2 className="text-lg font-bold text-[#0B1E3F]">{t("executiveMeetings.scheduleHeading")} — {date}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFullscreen(false)}
|
||||
className="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white text-[#0B1E3F] px-3 py-1.5 text-sm hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
ESC
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="hidden print:block text-center mb-2">
|
||||
<div className="font-bold text-lg">{t("executiveMeetings.title")}</div>
|
||||
@@ -2666,6 +2703,7 @@ function ScheduleSection({
|
||||
ref={tableWrapperRef}
|
||||
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"
|
||||
style={{ transform: `scale(${tableZoom / 100})`, transformOrigin: isRtl ? "top right" : "top left" }}
|
||||
>
|
||||
<table
|
||||
className="border-collapse text-sm md:text-[15px] w-full table-auto xl:table-fixed print:table-fixed"
|
||||
|
||||
Reference in New Issue
Block a user