Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/editor/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,45 @@
/* Print
--------------------------------------------------------------------------*/

/* ─── Export PDF menu ────────────────────────────────────────────────── */

.export-pdf {
position: relative;
}

.export-pdf__menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: 0 16px 32px -8px rgba(0, 0, 0, 0.35);
list-style: none;
margin: 0;
padding: 6px;
min-width: 200px;
z-index: 50;
}

.export-pdf__item {
appearance: none;
background: transparent;
border: none;
width: 100%;
text-align: left;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
color: var(--text);
cursor: pointer;
letter-spacing: 0.04em;
}

.export-pdf__item:hover {
background: var(--line);
}

/* ─── Lint warnings strip ─────────────────────────────────────────────── */

.editor__lint {
Expand Down
108 changes: 89 additions & 19 deletions src/render/ExportPdf.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,95 @@
'use client';

import { useEffect, useRef, useState } from 'react';

type WatermarkMode = 'none' | 'draft' | 'confidential' | 'review' | 'internal';

const OPTIONS: { value: WatermarkMode; label: string }[] = [
{ value: 'none', label: 'No watermark' },
{ value: 'draft', label: 'DRAFT' },
{ value: 'confidential', label: 'CONFIDENTIAL' },
{ value: 'review', label: 'FOR REVIEW' },
{ value: 'internal', label: 'INTERNAL ONLY' },
];

export function ExportPdf({ className }: { className?: string }) {
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!open) return;
const onClick = (e: MouseEvent) => {
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
window.addEventListener('mousedown', onClick);
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('mousedown', onClick);
window.removeEventListener('keydown', onKey);
};
}, [open]);

const exportWith = (mode: WatermarkMode) => {
setOpen(false);
if (typeof window === 'undefined') return;
const root = document.documentElement;
if (mode !== 'none') {
const label = OPTIONS.find((o) => o.value === mode)?.label ?? mode.toUpperCase();
root.setAttribute('data-watermark', mode);
root.style.setProperty('--watermark-text', `'${label}'`);
} else {
root.removeAttribute('data-watermark');
root.style.removeProperty('--watermark-text');
}
setTimeout(() => {
window.print();
window.setTimeout(() => {
root.removeAttribute('data-watermark');
root.style.removeProperty('--watermark-text');
}, 250);
}, 16);
};

return (
<button
type="button"
className={className}
onClick={() => {
if (typeof window !== 'undefined') window.print();
}}
aria-label="Export deck as PDF"
>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden>
<path
d="M6.5 1.5V8.5M6.5 8.5L3.5 5.5M6.5 8.5L9.5 5.5M2 10.5V11.5H11V10.5"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>Export PDF</span>
</button>
<div className="export-pdf" ref={wrapRef}>
<button
type="button"
className={className}
onClick={() => setOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={open}
aria-label="Export deck as PDF"
>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden>
<path
d="M6.5 1.5V8.5M6.5 8.5L3.5 5.5M6.5 8.5L9.5 5.5M2 10.5V11.5H11V10.5"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span>Export PDF</span>
</button>
{open ? (
<ul className="export-pdf__menu" role="menu">
{OPTIONS.map((opt) => (
<li key={opt.value}>
<button
type="button"
role="menuitem"
className="export-pdf__item"
onClick={() => exportWith(opt.value)}
>
{opt.label}
</button>
</li>
))}
</ul>
) : null}
</div>
);
}
29 changes: 29 additions & 0 deletions src/styles/deck.css
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,33 @@
.no-print {
display: none !important;
}

/* Watermark overlay on every printed slide. The text comes from a CSS
custom property set by the export menu; rotated, low-opacity, behind
content. */
html[data-watermark] .slide-frame {
position: relative;
}

html[data-watermark] .slide-frame::after {
content: var(--watermark-text, 'CONFIDENTIAL');
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: var(--font-display, ui-sans-serif);
font-weight: 800;
letter-spacing: 0.18em;
font-size: 156px;
color: rgba(0, 0, 0, 0.07);
transform: rotate(-28deg);
transform-origin: center;
pointer-events: none;
z-index: 5;
white-space: nowrap;
}

html[data-watermark='draft'] .slide-frame::after {
color: rgba(220, 38, 38, 0.08);
}
}
Loading