Skip to content

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:

typescript
// 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

PrintLayout

Wrapper for all print views with headers, footers, and page breaks:

typescript
// 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:

typescript
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:

typescript
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

typescript
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

typescript
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

typescript
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

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 */
  }
}

1. User Triggers Print

typescript
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:

html
<!-- 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

typescript
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:

typescript
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 PrintLayout wrapper for all print views
  • Test print output in browser print preview
  • Use page-break-avoid for 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():

typescript
setTimeout(() => window.print(), 100);

Issue: Tables split awkwardly

Cause: Default page break behavior.

Solution: Use CSS classes:

css
.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 activeTab prop
  • Uses a switch to 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

FunctionData SourceNotes
exportContactsCSVpersonnelSkips deleted, includes email/phone
exportScheduleCSVrehearsalScheduleExcludes deadline-type events
exportDeadlinesCSVrehearsalScheduleDeadline events only, optional departmentId filter
exportCostumesCSVcostumePiecesResolves character/scene names
exportSoundCuesCSVsoundCuesResolves scene names
exportTrackingCSVpersonnel + trackingColumnsCheckbox values as Yes/No
exportLightingCuesCSVlightingCuesResolves scene names; skips deleted
exportFollowSpotCuesCSVfollowSpotCues + followSpotsResolves 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:

typescript
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)