watermark:all the features completed

This commit is contained in:
saqib mir
2026-08-13 10:47:42 +05:30
parent 59854f8f61
commit 6c22179699
4 changed files with 499 additions and 27 deletions
+13 -3
View File
@@ -117,6 +117,7 @@ function App() {
const [isOCRLoading, setIsOCRLoading] = useState(false);
const [creatorPageCount, setCreatorPageCount] = useState<number>(1);
const [creatorPages, setCreatorPages] = useState<PageLayout[]>([]);
const [watermarkPreview, setWatermarkPreview] = useState<WatermarkConfig | null>(null);
const handleCreatorPageCountChange = useCallback((count: number, pages?: PageLayout[]) => {
setCreatorPageCount(count);
@@ -658,7 +659,9 @@ function App() {
const handleToolChange = async (tool: ToolId) => {
if (tool === 'watermark') {
setActiveTool('watermark');
setWatermarkModalOpen(true);
setInspectorTab('watermark');
setIsInspectorOpen(true);
setIsInspectorExpanded(true);
return;
}
if (tool === 'create_pdf') {
@@ -775,7 +778,7 @@ function App() {
const fitWidth = () => {
const w = activeDoc?.pageWidth || 612;
const inspectorWidth = isInspectorOpen ? 322 : 0;
const inspectorWidth = isInspectorOpen ? 360 : 0;
const avail = window.innerWidth - inspectorWidth - 48;
setZoom(Math.max(0.25, Math.min(3, avail / w)));
};
@@ -868,7 +871,11 @@ function App() {
onApplyRedactions={handleApplyRedactions}
onClearRedactions={() => setPendingRedactions([])}
onRedactPages={() => setRedactPagesModalOpen(true)}
onOpenWatermark={() => setWatermarkModalOpen(true)}
onOpenWatermark={() => {
setInspectorTab('watermark');
setIsInspectorOpen(true);
setIsInspectorExpanded(true);
}}
selectedAnnotation={annotations.find(a => a.id === selectedAnnotationId)}
onUpdateAnnotation={(patch) => {
const a = annotations.find(x => x.id === selectedAnnotationId);
@@ -937,6 +944,7 @@ function App() {
activeStamp={activeStamp}
annotations={annotations}
canCopy={can('canCopy')}
watermarkPreview={watermarkPreview}
onFieldChange={(id, value, i) => {
if (!can('canFillForms')) { denyToast('Filling form fields'); return; }
applyOps([{
@@ -1085,6 +1093,8 @@ function App() {
preservePageRef.current = true;
}
}}
onApplyWatermark={handleApplyWatermark}
onPreviewChange={setWatermarkPreview}
/>
)}
</div>
+367 -4
View File
@@ -9,10 +9,14 @@ import { EmptyState } from './ui';
import {
PagesIcon, NotesIcon, SearchIcon, PropertiesIcon, FontsIcon, OutlineIcon, FormsIcon,
ChevronDownIcon, ChevronUpIcon, SearchIcon as SearchGlyph, TrashIcon, HistoryIcon, XIcon, MoonIcon, SunIcon,
TextBoxIcon, ImageIcon,
} from './icons';
import { useTheme } from '../lib/useTheme';
export type InspectorTab = 'pages' | 'notes' | 'search' | 'properties' | 'fonts' | 'outline' | 'forms' | 'history';
import { ColorSwatches } from './ui';
import type { WatermarkConfig } from './WatermarkModal';
export type InspectorTab = 'pages' | 'notes' | 'search' | 'properties' | 'fonts' | 'outline' | 'forms' | 'history' | 'watermark';
interface InspectorPanelProps {
activeTab: InspectorTab;
@@ -58,6 +62,8 @@ interface InspectorPanelProps {
history?: { stack: string[]; index: number };
onRestoreHistory?: (docId: string) => void;
onApplyWatermark?: (config: WatermarkConfig, targetPageIndices: number[]) => Promise<void>;
onPreviewChange?: (config: WatermarkConfig | null) => void;
}
const TABS: { id: InspectorTab; label: string; icon: React.ReactNode; stub?: boolean }[] = [
@@ -80,7 +86,7 @@ function formatBytes(bytes?: number) {
export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
const selectedDoc = p.documents.find((d) => d.id === p.selectedDocumentId);
const activeDef = TABS.find((t) => t.id === p.activeTab)!;
const activeDef = TABS.find((t) => t.id === p.activeTab) || { id: p.activeTab, label: p.activeTab === 'watermark' ? 'Watermark' : p.activeTab, icon: null };
const { theme, toggleTheme } = useTheme();
return (
@@ -131,7 +137,7 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
{/* Expanded Content Drawer */}
{p.isExpanded && (
<div className="flex w-[322px] min-w-0 shrink-0 flex-col border-l border-border-primary">
<div className="flex w-[360px] min-w-0 shrink-0 flex-col border-l border-border-primary overflow-x-hidden">
<div className="flex h-[48px] shrink-0 items-center justify-between border-b border-border-primary" style={{ paddingLeft: '14px', paddingRight: '12px' }}>
<span className="shrink-0 text-[13px] font-bold text-text-primary">{activeDef.label}</span>
<CustomButton variant="unstyled" onClick={() => p.onExpandedChange(false)} className="flex h-7 w-7 items-center justify-center rounded-[6px] text-text-tertiary transition-colors hover:bg-bg-tertiary hover:text-text-primary">
@@ -139,7 +145,7 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
</CustomButton>
</div>
<div className="scroll-micro min-h-0 flex-1 overflow-y-auto">
<div className="scroll-micro min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
{p.activeTab === 'pages' && <PagesTab {...p} />}
{p.activeTab === 'notes' && <NotesTab annotations={p.annotations.filter((a) => a.type !== 'widget')} onNavigate={p.onNavigateAnnotation} onDelete={p.onDeleteAnnotation} onUpdate={p.onUpdateAnnotation} />}
{p.activeTab === 'search' && <SearchTab {...p} />}
@@ -148,6 +154,7 @@ export const InspectorPanel: React.FC<InspectorPanelProps> = (p) => {
{p.activeTab === 'outline' && <OutlineTab outline={p.outline} onNavigate={p.onNavigateOutline} />}
{p.activeTab === 'forms' && <FormsTab fields={p.annotations.filter((a) => a.type === 'widget')} onNavigate={p.onNavigateAnnotation} />}
{p.activeTab === 'history' && <HistoryTab history={p.history} onRestore={p.onRestoreHistory} />}
{p.activeTab === 'watermark' && <WatermarkTab totalPages={p.totalPages} currentPage={p.currentPage} onApplyWatermark={p.onApplyWatermark} onPreviewChange={p.onPreviewChange} />}
</div>
</div>
)}
@@ -690,4 +697,360 @@ const HistoryTab: React.FC<{ history?: { stack: string[]; index: number }; onRes
})}
</div>
);
};
const WatermarkTab: React.FC<{
totalPages: number;
currentPage: number;
onApplyWatermark?: (config: WatermarkConfig, targetPageIndices: number[]) => Promise<void>;
onPreviewChange?: (config: WatermarkConfig | null) => void;
}> = ({ totalPages, currentPage, onApplyWatermark, onPreviewChange }) => {
const [tab, setTab] = React.useState<'text' | 'image'>('text');
const [text, setText] = React.useState('CONFIDENTIAL');
const [fontFamily, setFontFamily] = React.useState('Helvetica');
const [fontSize, setFontSize] = React.useState(48);
const [fontWeight, setFontWeight] = React.useState('bold');
const [color, setColor] = React.useState('#7c3aed');
const [opacity, setOpacity] = React.useState(40);
const [rotation, setRotation] = React.useState(-45);
const [position, setPosition] = React.useState<'top_left' | 'top_center' | 'top_right' | 'center_left' | 'center' | 'center_right' | 'bottom_left' | 'bottom_center' | 'bottom_right'>('center');
const [targetPages, setTargetPages] = React.useState<'all' | 'current' | 'custom'>('all');
const [customRangeStr, setCustomRangeStr] = React.useState(`1-${totalPages || 1}`);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [successMessage, setSuccessMessage] = React.useState<string | null>(null);
React.useEffect(() => {
setCustomRangeStr(`1-${totalPages || 1}`);
}, [totalPages]);
React.useEffect(() => {
onPreviewChange?.({
text,
fontFamily,
fontSize,
fontWeight,
color,
opacity,
rotation,
position,
targetPages,
customRangeStr,
});
return () => {
onPreviewChange?.(null);
};
}, [text, fontFamily, fontSize, fontWeight, color, opacity, rotation, position, targetPages, customRangeStr, onPreviewChange]);
const parsePageIndices = (): { indices: number[]; error?: string } => {
if (targetPages === 'all') {
return { indices: Array.from({ length: Math.max(1, totalPages) }, (_, i) => i) };
}
if (targetPages === 'current') {
return { indices: [currentPage] };
}
const cleaned = customRangeStr.trim();
if (!cleaned) return { indices: [], error: 'Please enter a valid page range.' };
const indicesSet = new Set<number>();
const parts = cleaned.split(',');
for (const part of parts) {
const p = part.trim();
if (!p) continue;
if (p.includes('-')) {
const [startStr, endStr] = p.split('-');
const start = parseInt(startStr, 10);
const end = parseInt(endStr, 10);
if (isNaN(start) || isNaN(end) || start > end || start < 1 || end > totalPages) {
return { indices: [], error: `Invalid page range "${p}". Page numbers must be between 1 and ${totalPages}.` };
}
for (let i = start; i <= end; i++) {
indicesSet.add(i - 1);
}
} else {
const num = parseInt(p, 10);
if (isNaN(num) || num < 1 || num > totalPages) {
return { indices: [], error: `Invalid page number "${p}". Page numbers must be between 1 and ${totalPages}.` };
}
indicesSet.add(num - 1);
}
}
const result = Array.from(indicesSet).sort((a, b) => a - b);
if (result.length === 0) return { indices: [], error: 'No valid pages selected.' };
return { indices: result };
};
const handleApply = async () => {
if (isSubmitting || !onApplyWatermark) return;
const trimmedText = text.trim();
if (!trimmedText) {
setErrorMessage('Watermark text cannot be empty.');
return;
}
const { indices, error } = parsePageIndices();
if (error || indices.length === 0) {
setErrorMessage(error || 'Invalid page range.');
return;
}
try {
setIsSubmitting(true);
setErrorMessage(null);
setSuccessMessage(null);
await onApplyWatermark(
{
text: trimmedText,
fontFamily,
fontSize,
fontWeight,
color,
opacity,
rotation,
position,
targetPages,
customRangeStr,
},
indices
);
setSuccessMessage(`Watermark added to ${indices.length} page(s)!`);
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err: any) {
setErrorMessage(err.message || 'Failed to apply watermark.');
} finally {
setIsSubmitting(false);
}
};
const posGrid: { id: WatermarkConfig['position']; label: string }[] = [
{ id: 'top_left', label: 'TL' },
{ id: 'top_center', label: 'TC' },
{ id: 'top_right', label: 'TR' },
{ id: 'center_left', label: 'CL' },
{ id: 'center', label: 'C' },
{ id: 'center_right', label: 'CR' },
{ id: 'bottom_left', label: 'BL' },
{ id: 'bottom_center', label: 'BC' },
{ id: 'bottom_right', label: 'BR' },
];
return (
<div className="flex flex-col gap-4 p-4 text-[13px]">
{/* Sub Tabs: Text | Image */}
<div className="flex rounded-lg bg-bg-secondary p-1 border border-border-primary">
<button
type="button"
onClick={() => setTab('text')}
className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-[12px] font-bold transition-all ${
tab === 'text' ? 'bg-white text-brand-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
}`}
>
<TextBoxIcon size={14} /> Text
</button>
<button
type="button"
onClick={() => setTab('image')}
className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-[12px] font-bold transition-all ${
tab === 'image' ? 'bg-white text-brand-primary shadow-sm' : 'text-text-secondary hover:text-text-primary'
}`}
>
<ImageIcon size={14} /> Image
</button>
</div>
{errorMessage && (
<div className="rounded-lg bg-red-50 border border-red-200 p-2.5 text-red-700 text-xs font-semibold flex items-center justify-between">
<span> {errorMessage}</span>
<button type="button" onClick={() => setErrorMessage(null)} className="text-red-500 font-bold ml-2"></button>
</div>
)}
{successMessage && (
<div className="rounded-lg bg-emerald-50 border border-emerald-200 p-2.5 text-emerald-700 text-xs font-semibold flex items-center justify-between">
<span> {successMessage}</span>
<button type="button" onClick={() => setSuccessMessage(null)} className="text-emerald-500 font-bold ml-2"></button>
</div>
)}
{tab === 'text' ? (
<>
{/* Text Input */}
<div className="flex flex-col gap-1.5">
<label className="text-[11.5px] font-bold text-text-primary">Text</label>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
disabled={isSubmitting}
placeholder="Enter watermark text..."
className="h-9 w-full rounded-lg border border-border-primary bg-bg-primary px-3 text-[13px] text-text-primary outline-none transition-all focus:border-brand-primary focus:ring-1 focus:ring-brand-primary font-medium"
/>
<div className="flex flex-wrap items-center gap-1 pt-1">
{['CONFIDENTIAL', 'DRAFT', 'SAMPLE', 'PROPERTY OF COMPANY', 'FINAL', 'INTERNAL USE ONLY'].map((preset) => (
<button
key={preset}
type="button"
onClick={() => setText(preset)}
className={`rounded px-2 py-0.5 text-[10.5px] font-semibold border transition-all cursor-pointer ${
text === preset
? 'bg-brand-primary text-white border-brand-primary'
: 'bg-bg-secondary text-text-secondary border-border-primary hover:bg-bg-tertiary'
}`}
>
{preset}
</button>
))}
</div>
</div>
{/* Text Format */}
<div className="flex flex-col gap-2 rounded-xl border border-border-primary bg-bg-secondary/40 p-3">
<span className="text-[11px] font-bold uppercase tracking-wider text-text-tertiary">Text Format</span>
<div className="grid grid-cols-2 gap-2">
<select
value={fontFamily}
onChange={(e) => setFontFamily(e.target.value)}
disabled={isSubmitting}
className="h-8 rounded-lg border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none"
>
<option value="Helvetica">Helvetica</option>
<option value="Times-Roman">Times New Roman</option>
<option value="Courier">Courier</option>
<option value="Arial">Arial</option>
</select>
<select
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
disabled={isSubmitting}
className="h-8 rounded-lg border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none"
>
{[16, 24, 36, 48, 72, 96, 120].map((sz) => (
<option key={sz} value={sz}>{sz} pt</option>
))}
</select>
</div>
<div className="flex flex-wrap items-center justify-between gap-2 pt-1">
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => setFontWeight((w) => (w === 'bold' ? 'normal' : 'bold'))}
className={`h-7 w-7 rounded-lg font-bold text-[12px] border transition-colors cursor-pointer ${
fontWeight === 'bold' ? 'bg-brand-primary text-white border-brand-primary' : 'bg-bg-primary text-text-secondary border-border-primary hover:bg-bg-tertiary'
}`}
>
B
</button>
<span className="text-[11px] font-semibold text-text-secondary">Color:</span>
</div>
<div className="flex items-center gap-1 shrink-0 min-w-0">
<ColorSwatches
value={color}
onChange={setColor}
palette={['#7c3aed', '#dc2626', '#2563eb', '#16a34a', '#d97706', '#18212e', '#ffffff']}
/>
</div>
</div>
</div>
{/* Position (3x3 Grid) */}
<div className="flex flex-col gap-1.5">
<label className="text-[11.5px] font-bold text-text-primary">Position</label>
<div className="grid grid-cols-3 gap-1.5 w-full max-w-[210px] mx-auto p-2 bg-bg-secondary rounded-xl border border-border-primary">
{posGrid.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setPosition(p.id)}
className={`h-10 rounded-lg flex items-center justify-center transition-all cursor-pointer ${
position === p.id
? 'bg-brand-primary text-white shadow-md ring-2 ring-brand-primary/30'
: 'bg-white text-text-secondary hover:bg-bg-tertiary border border-border-primary'
}`}
>
<span className="h-2.5 w-2.5 rounded-full bg-current" />
</button>
))}
</div>
</div>
{/* Rotation & Transparency */}
<div className="grid grid-cols-2 gap-3 items-center pt-1">
<div className="flex flex-col gap-1">
<label className="text-[11.5px] font-bold text-text-primary">Rotation</label>
<select
value={rotation}
onChange={(e) => setRotation(Number(e.target.value))}
disabled={isSubmitting}
className="h-9 rounded-lg border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none transition-all focus:border-brand-primary focus:ring-1 focus:ring-brand-primary"
>
<option value={0}>Do not rotate</option>
<option value={45}>45 degrees</option>
<option value={90}>90 degrees</option>
<option value={180}>180 degrees</option>
<option value={270}>270 degrees</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-[11.5px] font-bold text-text-primary">Transparency</label>
<select
value={opacity}
onChange={(e) => setOpacity(Number(e.target.value))}
disabled={isSubmitting}
className="h-9 rounded-lg border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none transition-all focus:border-brand-primary focus:ring-1 focus:ring-brand-primary"
>
<option value={100}>No transparency</option>
<option value={75}>75%</option>
<option value={50}>50%</option>
<option value={25}>25%</option>
</select>
</div>
</div>
{/* Target Pages */}
<div className="flex flex-col gap-1 pt-1">
<label className="text-[11.5px] font-bold text-text-primary">Apply to Pages</label>
<select
value={targetPages}
onChange={(e) => setTargetPages(e.target.value as any)}
disabled={isSubmitting}
className="h-9 rounded-lg border border-border-primary bg-bg-primary px-2.5 text-[12px] font-medium text-text-primary outline-none"
>
<option value="all">All Pages ({totalPages})</option>
<option value="current">Current Page ({currentPage + 1})</option>
<option value="custom">Custom Page Range</option>
</select>
</div>
{targetPages === 'custom' && (
<input
type="text"
value={customRangeStr}
onChange={(e) => setCustomRangeStr(e.target.value)}
placeholder={`1-${totalPages}`}
className="h-8 w-full rounded-lg border border-border-primary bg-bg-primary px-2.5 text-[12px] text-text-primary outline-none"
/>
)}
{/* Primary Action Button */}
<button
type="button"
onClick={handleApply}
disabled={isSubmitting}
className="mt-3 flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-brand-primary text-white text-[13.5px] font-bold shadow-md hover:bg-brand-primary/95 active:scale-[0.99] transition-all cursor-pointer disabled:opacity-50"
>
{isSubmitting ? 'Adding Watermark...' : 'Add Watermark →'}
</button>
</>
) : (
<div className="flex flex-col items-center justify-center p-6 text-center text-text-tertiary">
<span className="text-3xl mb-2">🖼</span>
<p className="font-bold text-[13px] text-text-primary mb-1">Image Watermark</p>
<p className="text-[11.5px]">Upload an image logo or stamp to place across PDF pages.</p>
</div>
)}
</div>
);
};
+26 -20
View File
@@ -212,9 +212,10 @@ export const WatermarkModal: React.FC<WatermarkModalProps> = ({
disabled={isSubmitting}
className="h-8 rounded-md border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none"
>
<option value="Helvetica">Helvetica (Sans-Serif)</option>
<option value="Times-Roman">Times-Roman (Serif)</option>
<option value="Courier">Courier (Monospace)</option>
<option value="Helvetica">Helvetica</option>
<option value="Times-Roman">Times New Roman</option>
<option value="Courier">Courier</option>
<option value="Arial">Arial</option>
</select>
</div>
@@ -251,29 +252,34 @@ export const WatermarkModal: React.FC<WatermarkModalProps> = ({
</div>
<div className="grid grid-cols-2 gap-3 items-center">
<Slider
label="Opacity"
min={5}
max={100}
step={5}
value={opacity}
onChange={setOpacity}
suffix="%"
width={100}
/>
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold text-text-secondary">Rotation:</span>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold text-text-secondary">Rotation:</label>
<select
value={rotation}
onChange={(e) => setRotation(Number(e.target.value))}
disabled={isSubmitting}
className="h-8 rounded-md border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none"
>
<option value={-45}>-45° Diagonal</option>
<option value={45}>45° Diagonal</option>
<option value={0}>0° Horizontal</option>
<option value={90}>90° Vertical</option>
<option value={0}>Do not rotate</option>
<option value={45}>45 degrees</option>
<option value={90}>90 degrees</option>
<option value={180}>180 degrees</option>
<option value={270}>270 degrees</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] font-semibold text-text-secondary">Transparency:</label>
<select
value={opacity}
onChange={(e) => setOpacity(Number(e.target.value))}
disabled={isSubmitting}
className="h-8 rounded-md border border-border-primary bg-bg-primary px-2 text-[12px] font-medium text-text-primary outline-none"
>
<option value={100}>No transparency</option>
<option value={75}>75%</option>
<option value={50}>50%</option>
<option value={25}>25%</option>
</select>
</div>
</div>
+93
View File
@@ -97,6 +97,7 @@ interface PDFViewerProps {
layoutData?: PageLayoutResponse | null;
availableFonts?: FontInfo[];
onEditBlock?: (pageIndex: number, payload: BlockEditPayload) => void;
watermarkPreview?: import('../components/WatermarkModal').WatermarkConfig | null;
}
interface PageLayout {
@@ -154,6 +155,7 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(
onDecorateText,
onFieldChange,
canCopy = true,
watermarkPreview = null,
},
ref,
) => {
@@ -1017,6 +1019,15 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(
searchCurrentMatch={searchCurrentMatch}
/>
{watermarkPreview && isPageTargetedByWatermark(page.index, watermarkPreview) && (
<WatermarkPreviewOverlay
preview={watermarkPreview}
width={page.width}
height={page.height}
zoom={zoom}
/>
)}
{/* Interactive signature placement overlay */}
{activePlacement &&
activePlacement.pageIndex === page.index &&
@@ -1064,3 +1075,85 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(
);
},
);
const isPageTargetedByWatermark = (
pageIndex: number,
preview: import('../components/WatermarkModal').WatermarkConfig
): boolean => {
if (preview.targetPages === 'all') return true;
if (preview.targetPages === 'current') return true;
if (preview.targetPages === 'custom') {
const parts = (preview.customRangeStr || '').split(',');
for (const part of parts) {
const p = part.trim();
if (!p) continue;
if (p.includes('-')) {
const [s, e] = p.split('-').map((x) => parseInt(x, 10));
if (!isNaN(s) && !isNaN(e) && pageIndex >= s - 1 && pageIndex <= e - 1) return true;
} else {
const num = parseInt(p, 10);
if (!isNaN(num) && pageIndex === num - 1) return true;
}
}
}
return false;
};
const WatermarkPreviewOverlay: React.FC<{
preview: import('../components/WatermarkModal').WatermarkConfig;
width: number;
height: number;
zoom: number;
}> = ({ preview, width, height, zoom }) => {
if (!preview.text.trim()) return null;
let justifyContent = 'center';
let alignItems = 'center';
if (preview.position.includes('left')) justifyContent = 'flex-start';
if (preview.position.includes('right')) justifyContent = 'flex-end';
if (preview.position.includes('top')) alignItems = 'flex-start';
if (preview.position.includes('bottom')) alignItems = 'flex-end';
const margin = 36 * zoom;
const style: React.CSSProperties = {
padding: `${margin}px`,
};
const scaledFontSize = preview.fontSize * zoom;
const opacityVal = preview.opacity / 100.0;
return (
<div
className="absolute inset-0 pointer-events-none z-30 flex overflow-hidden select-none"
style={{
width: `${width * zoom}px`,
height: `${height * zoom}px`,
justifyContent,
alignItems,
...style,
}}
>
<div
className="transition-all duration-150 transform-gpu flex items-center justify-center tracking-wide"
style={{
fontFamily: preview.fontFamily.includes('Times')
? 'Times New Roman, serif'
: preview.fontFamily.includes('Courier')
? 'Courier New, monospace'
: 'Inter, Helvetica, sans-serif',
fontSize: `${scaledFontSize}px`,
fontWeight: preview.fontWeight === 'bold' ? 'bold' : 'normal',
color: preview.color,
opacity: opacityVal,
transform: `rotate(${preview.rotation}deg)`,
textShadow: '0 0 4px rgba(0,0,0,0.15)',
whiteSpace: 'nowrap',
}}
>
{preview.text}
</div>
</div>
);
};