Print System Architecture
Professional PDF generation with print-optimized primitives.
Overview
The Print System provides feature-specific print views with clean formatting, proper pagination, and logo placement. It uses a portal-based rendering pattern to generate print-optimized layouts.
Core Architecture
Portal-Based Rendering
Print views render into a hidden portal that's styled for print media:
// PrintPortal.tsx
import { createPortal } from 'react-dom';
export const PrintPortal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const portalRoot = document.getElementById('print-root');
return createPortal(
<div className="print-container">
{children}
</div>,
portalRoot!
);
};Why portals?
- Print layout doesn't affect main UI
- Can use different component tree
- CSS print media queries apply cleanly
Print-Optimized Primitives
PrintLayout
Wrapper for all print views with headers, footers, and page breaks:
// src/components/common/print/PrintLayout.tsx
interface PrintLayoutProps {
title: string;
showLogo?: boolean;
children: React.ReactNode;
}
export const PrintLayout: React.FC<PrintLayoutProps> = ({
title,
showLogo = true,
children
}) => {
return (
<div className="print-layout">
<PrintHeader title={title} showLogo={showLogo} />
<div className="print-content">
{children}
</div>
<PrintFooter />
</div>
);
};PrintTable
Table component with automatic page breaks:
interface PrintTableProps {
headers: string[];
rows: React.ReactNode[][];
}
export const PrintTable: React.FC<PrintTableProps> = ({ headers, rows }) => {
return (
<table className="print-table">
<thead>
<tr>
{headers.map((h) => <th key={h}>{h}</th>)}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className="print-row">
{row.map((cell, j) => <td key={j}>{cell}</td>)}
</tr>
))}
</tbody>
</table>
);
};PrintHeader
Consistent header with project name and logo:
export const PrintHeader: React.FC<{ title: string; showLogo?: boolean }> = ({
title,
showLogo
}) => {
const { projectName, theaterLogo } = useStore((s) => s.meta);
return (
<header className="print-header">
{showLogo && theaterLogo && <img src={theaterLogo} alt="Logo" />}
<h1>{projectName}</h1>
<h2>{title}</h2>
</header>
);
};Feature-Specific Print Views
Run Sheet Print
Located: src/features/print/RunSheetPrint.tsx
export const RunSheetPrint: React.FC = () => {
const rows = useStore((s) => s.runSheet.rows);
return (
<PrintPortal>
<PrintLayout title="Run Sheet">
<PrintTable
headers={['Scene #', 'Moment', 'Set/Strike', 'Notes']}
rows={rows.map(r => [r.scene, r.moment, r.setStrike, r.notes])}
/>
</PrintLayout>
</PrintPortal>
);
};Props Print
Located: src/features/print/PropsPrint.tsx
export const PropsPrint: React.FC = () => {
const props = useStore((s) => s.props.items);
return (
<PrintPortal>
<PrintLayout title="Props List">
<PrintTable
headers={['Item', 'Type', 'Scene', 'Entrance', 'Exit']}
rows={props.map(p => [
p.name,
p.type,
p.scene,
p.entrance,
p.exit
])}
/>
</PrintLayout>
</PrintPortal>
);
};Scheduler Print (with Filtering)
Located: src/features/print/SchedulerPrint.tsx
type FilterMode = 'production' | 'rehearsal' | 'full';
export const SchedulerPrint: React.FC<{ filter: FilterMode }> = ({ filter }) => {
const events = useStore((s) => s.scheduler.events);
const filteredEvents = events.filter(e => {
if (filter === 'production') return ['meeting', 'deadline'].includes(e.type);
if (filter === 'rehearsal') return ['rehearsal', 'tech', 'performance'].includes(e.type);
return true; // 'full'
});
return (
<PrintPortal>
<PrintLayout title={`Schedule (${filter})`}>
{/* Render calendar grid */}
</PrintLayout>
</PrintPortal>
);
};CSS Print Styles
Global Print Styles
Located: src/index.css
@media print {
/* Hide UI chrome */
nav, header, .no-print {
display: none !important;
}
/* Print container */
.print-container {
display: block !important;
}
/* Page breaks */
.page-break {
page-break-after: always;
}
.avoid-break {
page-break-inside: avoid;
}
/* Typography */
body {
font-size: 10pt;
line-height: 1.3;
color: black;
background: white;
}
/* Tables */
.print-table {
width: 100%;
border-collapse: collapse;
}
.print-table th,
.print-table td {
border: 1px solid #333;
padding: 4pt;
text-align: left;
}
.print-table thead {
display: table-header-group; /* Repeat on every page */
}
}Print Workflow
1. User Triggers Print
const handlePrint = () => {
// Render print view into portal
setPrintMode(true);
// Wait for portal render
setTimeout(() => {
window.print();
}, 100);
};2. Portal Renders
Print component mounts into #print-root:
<!-- index.html -->
<div id="root"></div>
<div id="print-root" class="print-container"></div>3. Browser Print Dialog
Browser's native print dialog opens with:
- Print-optimized layout visible
- Main UI hidden via CSS
- Page breaks respected
4. Cleanup
const handleAfterPrint = () => {
setPrintMode(false); // Unmount print portal
};
window.addEventListener('afterprint', handleAfterPrint);Custom Report Templates (Roadmap)
Future feature: Block-Based Template Creator
Users will assemble custom reports from pre-defined blocks:
interface ReportBlock {
type: 'header' | 'table' | 'text' | 'image';
config: BlockConfig;
}
// Example: Custom call sheet
const callSheet: ReportBlock[] = [
{ type: 'header', config: { title: 'Call Sheet' }},
{ type: 'table', config: { source: 'scheduler', filter: 'today' }},
{ type: 'text', config: { content: 'Notes...' }},
];Template Builder implementation is on the roadmap.
Best Practices
✅ Do
- Use
PrintLayoutwrapper for all print views - Test print output in browser print preview
- Use
page-break-avoidfor tables that shouldn't split - Include project name and date in headers
❌ Don't
- Hardcode page dimensions (let browser handle it)
- Use colored backgrounds (wastes ink)
- Rely on JavaScript for layout (CSS print media queries only)
Troubleshooting
Issue: Print layout is blank
Cause: Portal not rendered before print.
Solution: Add delay before window.print():
setTimeout(() => window.print(), 100);Issue: Tables split awkwardly
Cause: Default page break behavior.
Solution: Use CSS classes:
.print-table tbody tr {
page-break-inside: avoid;
}CSV Export System
In addition to print-based PDF output, the system supports per-tab CSV export via the ExportCSVButton component.
Architecture
┌─────────────────────┐
│ ExportCSVButton │──────► Maps activeTab → export function
│ (Action Toolbar) │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ csv-utils.ts │──────► 8 export functions
│ (export functions) │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ downloadCSV() │──────► Blob → <a> click → file download
└─────────────────────┘ExportCSVButton Component
Located: src/features/print/components/ExportCSVButton.tsx
- Accepts
activeTabprop - Uses a
switchto dispatch the correct export function - Reads store data inline via
useAppStore.getState() - Shows success/error toast via
showToast - Production tab exports two files (deadlines + tracking)
Export Functions
Located: src/modules/csv-utils.ts
| Function | Data Source | Notes |
|---|---|---|
exportContactsCSV | personnel | Skips deleted, includes email/phone |
exportScheduleCSV | rehearsalSchedule | Excludes deadline-type events |
exportDeadlinesCSV | rehearsalSchedule | Deadline events only, optional departmentId filter |
exportCostumesCSV | costumePieces | Resolves character/scene names |
exportSoundCuesCSV | soundCues | Resolves scene names |
exportTrackingCSV | personnel + trackingColumns | Checkbox values as Yes/No |
exportLightingCuesCSV | lightingCues | Resolves scene names; skips deleted |
exportFollowSpotCuesCSV | followSpotCues + followSpots | Resolves spot labels, character names, scene names |
All functions use escapeCSV() for proper quoting and downloadCSV() for Blob-based file download.
Toolbar Integration
The ExportCSVButton renders in MainLayout.tsx alongside the print button, gated by toolHasCSVExport:
const toolHasCSVExport: Partial<Record<ToolId, boolean>> = {
'scenes': true, 'props': true, 'cast': true,
'scheduler': true, 'calendar': true, 'costumes': true,
'sound': true, 'production': true, 'lighting': true,
};Further Reading
Last updated: February 8, 2026 (Added LX Cue List, Follow Spot Sheet reports, and 2 lighting CSV export functions)