From 2f1d1ba3c41073faac6e095e894ce26bc6e649df Mon Sep 17 00:00:00 2001 From: developeranku Date: Wed, 6 May 2026 19:13:52 +0530 Subject: [PATCH] feat: PDF watermark modes (DRAFT, CONFIDENTIAL, FOR REVIEW, INTERNAL) --- src/editor/editor.css | 39 ++++++++++++++ src/render/ExportPdf.tsx | 108 ++++++++++++++++++++++++++++++++------- src/styles/deck.css | 29 +++++++++++ 3 files changed, 157 insertions(+), 19 deletions(-) diff --git a/src/editor/editor.css b/src/editor/editor.css index 9b99467..a331f48 100644 --- a/src/editor/editor.css +++ b/src/editor/editor.css @@ -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 { diff --git a/src/render/ExportPdf.tsx b/src/render/ExportPdf.tsx index 7f79951..6fe7b7b 100644 --- a/src/render/ExportPdf.tsx +++ b/src/render/ExportPdf.tsx @@ -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(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 ( - +
+ + {open ? ( +
    + {OPTIONS.map((opt) => ( +
  • + +
  • + ))} +
+ ) : null} +
); } diff --git a/src/styles/deck.css b/src/styles/deck.css index c8db9ca..46f96f4 100644 --- a/src/styles/deck.css +++ b/src/styles/deck.css @@ -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); + } }