Skip to content

Commit c61d3b5

Browse files
feat: PDF watermark modes (DRAFT, CONFIDENTIAL, FOR REVIEW, INTERNAL) (#15)
1 parent b144833 commit c61d3b5

3 files changed

Lines changed: 157 additions & 19 deletions

File tree

src/editor/editor.css

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,45 @@
14251425
/* Print
14261426
--------------------------------------------------------------------------*/
14271427

1428+
/* ─── Export PDF menu ────────────────────────────────────────────────── */
1429+
1430+
.export-pdf {
1431+
position: relative;
1432+
}
1433+
1434+
.export-pdf__menu {
1435+
position: absolute;
1436+
top: calc(100% + 6px);
1437+
right: 0;
1438+
background: var(--surface);
1439+
border: 1px solid var(--line);
1440+
border-radius: 8px;
1441+
box-shadow: 0 16px 32px -8px rgba(0, 0, 0, 0.35);
1442+
list-style: none;
1443+
margin: 0;
1444+
padding: 6px;
1445+
min-width: 200px;
1446+
z-index: 50;
1447+
}
1448+
1449+
.export-pdf__item {
1450+
appearance: none;
1451+
background: transparent;
1452+
border: none;
1453+
width: 100%;
1454+
text-align: left;
1455+
padding: 8px 12px;
1456+
border-radius: 6px;
1457+
font-size: 13px;
1458+
color: var(--text);
1459+
cursor: pointer;
1460+
letter-spacing: 0.04em;
1461+
}
1462+
1463+
.export-pdf__item:hover {
1464+
background: var(--line);
1465+
}
1466+
14281467
/* ─── Lint warnings strip ─────────────────────────────────────────────── */
14291468

14301469
.editor__lint {

src/render/ExportPdf.tsx

Lines changed: 89 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,95 @@
11
'use client';
22

3+
import { useEffect, useRef, useState } from 'react';
4+
5+
type WatermarkMode = 'none' | 'draft' | 'confidential' | 'review' | 'internal';
6+
7+
const OPTIONS: { value: WatermarkMode; label: string }[] = [
8+
{ value: 'none', label: 'No watermark' },
9+
{ value: 'draft', label: 'DRAFT' },
10+
{ value: 'confidential', label: 'CONFIDENTIAL' },
11+
{ value: 'review', label: 'FOR REVIEW' },
12+
{ value: 'internal', label: 'INTERNAL ONLY' },
13+
];
14+
315
export function ExportPdf({ className }: { className?: string }) {
16+
const [open, setOpen] = useState(false);
17+
const wrapRef = useRef<HTMLDivElement>(null);
18+
19+
useEffect(() => {
20+
if (!open) return;
21+
const onClick = (e: MouseEvent) => {
22+
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false);
23+
};
24+
const onKey = (e: KeyboardEvent) => {
25+
if (e.key === 'Escape') setOpen(false);
26+
};
27+
window.addEventListener('mousedown', onClick);
28+
window.addEventListener('keydown', onKey);
29+
return () => {
30+
window.removeEventListener('mousedown', onClick);
31+
window.removeEventListener('keydown', onKey);
32+
};
33+
}, [open]);
34+
35+
const exportWith = (mode: WatermarkMode) => {
36+
setOpen(false);
37+
if (typeof window === 'undefined') return;
38+
const root = document.documentElement;
39+
if (mode !== 'none') {
40+
const label = OPTIONS.find((o) => o.value === mode)?.label ?? mode.toUpperCase();
41+
root.setAttribute('data-watermark', mode);
42+
root.style.setProperty('--watermark-text', `'${label}'`);
43+
} else {
44+
root.removeAttribute('data-watermark');
45+
root.style.removeProperty('--watermark-text');
46+
}
47+
setTimeout(() => {
48+
window.print();
49+
window.setTimeout(() => {
50+
root.removeAttribute('data-watermark');
51+
root.style.removeProperty('--watermark-text');
52+
}, 250);
53+
}, 16);
54+
};
55+
456
return (
5-
<button
6-
type="button"
7-
className={className}
8-
onClick={() => {
9-
if (typeof window !== 'undefined') window.print();
10-
}}
11-
aria-label="Export deck as PDF"
12-
>
13-
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden>
14-
<path
15-
d="M6.5 1.5V8.5M6.5 8.5L3.5 5.5M6.5 8.5L9.5 5.5M2 10.5V11.5H11V10.5"
16-
stroke="currentColor"
17-
strokeWidth="1.3"
18-
strokeLinecap="round"
19-
strokeLinejoin="round"
20-
/>
21-
</svg>
22-
<span>Export PDF</span>
23-
</button>
57+
<div className="export-pdf" ref={wrapRef}>
58+
<button
59+
type="button"
60+
className={className}
61+
onClick={() => setOpen((v) => !v)}
62+
aria-haspopup="menu"
63+
aria-expanded={open}
64+
aria-label="Export deck as PDF"
65+
>
66+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden>
67+
<path
68+
d="M6.5 1.5V8.5M6.5 8.5L3.5 5.5M6.5 8.5L9.5 5.5M2 10.5V11.5H11V10.5"
69+
stroke="currentColor"
70+
strokeWidth="1.3"
71+
strokeLinecap="round"
72+
strokeLinejoin="round"
73+
/>
74+
</svg>
75+
<span>Export PDF</span>
76+
</button>
77+
{open ? (
78+
<ul className="export-pdf__menu" role="menu">
79+
{OPTIONS.map((opt) => (
80+
<li key={opt.value}>
81+
<button
82+
type="button"
83+
role="menuitem"
84+
className="export-pdf__item"
85+
onClick={() => exportWith(opt.value)}
86+
>
87+
{opt.label}
88+
</button>
89+
</li>
90+
))}
91+
</ul>
92+
) : null}
93+
</div>
2494
);
2595
}

src/styles/deck.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,33 @@
169169
.no-print {
170170
display: none !important;
171171
}
172+
173+
/* Watermark overlay on every printed slide. The text comes from a CSS
174+
custom property set by the export menu; rotated, low-opacity, behind
175+
content. */
176+
html[data-watermark] .slide-frame {
177+
position: relative;
178+
}
179+
180+
html[data-watermark] .slide-frame::after {
181+
content: var(--watermark-text, 'CONFIDENTIAL');
182+
position: absolute;
183+
inset: 0;
184+
display: grid;
185+
place-items: center;
186+
font-family: var(--font-display, ui-sans-serif);
187+
font-weight: 800;
188+
letter-spacing: 0.18em;
189+
font-size: 156px;
190+
color: rgba(0, 0, 0, 0.07);
191+
transform: rotate(-28deg);
192+
transform-origin: center;
193+
pointer-events: none;
194+
z-index: 5;
195+
white-space: nowrap;
196+
}
197+
198+
html[data-watermark='draft'] .slide-frame::after {
199+
color: rgba(220, 38, 38, 0.08);
200+
}
172201
}

0 commit comments

Comments
 (0)