Feature/v2/watermark (#4215)

Add watermark feature

Auto scroll on review

---------

Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
ConnorYoh
2025-08-19 10:31:44 +01:00
committed by GitHub
co-authored by Connor Yoh
parent acbebd67a3
commit c1b7911518
24 changed files with 1410 additions and 78 deletions
@@ -0,0 +1,45 @@
import React, { useRef } from "react";
import { FileButton, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
interface FileUploadButtonProps {
file?: File;
onChange: (file: File | null) => void;
accept?: string;
disabled?: boolean;
placeholder?: string;
variant?: "outline" | "filled" | "light" | "default" | "subtle" | "gradient";
fullWidth?: boolean;
}
const FileUploadButton = ({
file,
onChange,
accept = "*/*",
disabled = false,
placeholder,
variant = "outline",
fullWidth = true
}: FileUploadButtonProps) => {
const { t } = useTranslation();
const resetRef = useRef<() => void>(null);
const defaultPlaceholder = t('chooseFile', 'Choose File');
return (
<FileButton
resetRef={resetRef}
onChange={onChange}
accept={accept}
disabled={disabled}
>
{(props) => (
<Button {...props} variant={variant} fullWidth={fullWidth}>
{file ? file.name : (placeholder || defaultPlaceholder)}
</Button>
)}
</FileButton>
);
};
export default FileUploadButton;
+2 -1
View File
@@ -2,7 +2,8 @@ import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
import { TooltipContent, TooltipTip } from './tooltip/TooltipContent';
import { TooltipTip } from '../../types/tips';
import { TooltipContent } from './tooltip/TooltipContent';
import { useSidebarContext } from '../../contexts/SidebarContext';
import styles from './tooltip/Tooltip.module.css'
@@ -1,12 +1,6 @@
import React from 'react';
import styles from './Tooltip.module.css';
export interface TooltipTip {
title?: string;
description?: string;
bullets?: string[];
body?: React.ReactNode;
}
import { TooltipTip } from '../../../types/tips';
interface TooltipContentProps {
content?: React.ReactNode;
@@ -0,0 +1,83 @@
import React from "react";
import { Stack, Checkbox, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
import NumberInputWithUnit from "../shared/NumberInputWithUnit";
interface WatermarkFormattingProps {
parameters: AddWatermarkParameters;
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
disabled?: boolean;
}
const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }: WatermarkFormattingProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
{/* Size - single row */}
<NumberInputWithUnit
label={t('watermark.settings.size', 'Size')}
value={parameters.fontSize}
onChange={(value) => onParameterChange('fontSize', typeof value === 'number' ? value : 12)}
unit={parameters.watermarkType === 'text' ? 'pt' : 'px'}
min={1}
disabled={disabled}
/>
{/* Position & Appearance - 2 per row */}
<Group grow align="flex-start">
<NumberInputWithUnit
label={t('watermark.settings.rotation', 'Rotation')}
value={parameters.rotation}
onChange={(value) => onParameterChange('rotation', typeof value === 'number' ? value : 0)}
unit="°"
min={-360}
max={360}
disabled={disabled}
/>
<NumberInputWithUnit
label={t('watermark.settings.opacity', 'Opacity')}
value={parameters.opacity}
onChange={(value) => onParameterChange('opacity', typeof value === 'number' ? value : 50)}
unit="%"
min={0}
max={100}
disabled={disabled}
/>
</Group>
{/* Spacing - 2 per row */}
<Group grow align="flex-start">
<NumberInputWithUnit
label={t('watermark.settings.spacing.horizontal', 'Horizontal Spacing')}
value={parameters.widthSpacer}
onChange={(value) => onParameterChange('widthSpacer', typeof value === 'number' ? value : 50)}
unit="px"
min={0}
max={200}
disabled={disabled}
/>
<NumberInputWithUnit
label={t('watermark.settings.spacing.vertical', 'Vertical Spacing')}
value={parameters.heightSpacer}
onChange={(value) => onParameterChange('heightSpacer', typeof value === 'number' ? value : 50)}
unit="px"
min={0}
max={200}
disabled={disabled}
/>
</Group>
{/* Advanced Options */}
<Checkbox
label={t('watermark.settings.convertToImage', 'Flatten PDF pages to images')}
checked={parameters.convertPDFToImage}
onChange={(event) => onParameterChange('convertPDFToImage', event.currentTarget.checked)}
disabled={disabled}
/>
</Stack>
);
};
export default WatermarkFormatting;
@@ -0,0 +1,29 @@
import React from "react";
import { Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
import FileUploadButton from "../../shared/FileUploadButton";
interface WatermarkImageFileProps {
parameters: AddWatermarkParameters;
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
disabled?: boolean;
}
const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }: WatermarkImageFileProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm">
<FileUploadButton
file={parameters.watermarkImage}
onChange={(file) => onParameterChange('watermarkImage', file)}
accept="image/*"
disabled={disabled}
placeholder={t('watermark.settings.image.choose', 'Choose Image')}
/>
</Stack>
);
};
export default WatermarkImageFile;
@@ -0,0 +1,63 @@
import React from "react";
import { Stack, Text, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
interface WatermarkStyleSettingsProps {
parameters: AddWatermarkParameters;
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
disabled?: boolean;
}
const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkStyleSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
{/* Appearance Settings */}
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.rotation', 'Rotation (degrees)')}</Text>
<NumberInput
value={parameters.rotation}
onChange={(value) => onParameterChange('rotation', value || 0)}
min={-360}
max={360}
disabled={disabled}
/>
<Text size="sm" fw={500}>{t('watermark.settings.opacity', 'Opacity (%)')}</Text>
<NumberInput
value={parameters.opacity}
onChange={(value) => onParameterChange('opacity', value || 50)}
min={0}
max={100}
disabled={disabled}
/>
</Stack>
{/* Spacing Settings */}
<Stack gap="sm">
<Text size="sm" fw={500}>{t('watermark.settings.spacing.width', 'Width Spacing')}</Text>
<NumberInput
value={parameters.widthSpacer}
onChange={(value) => onParameterChange('widthSpacer', value || 50)}
min={0}
max={200}
disabled={disabled}
/>
<Text size="sm" fw={500}>{t('watermark.settings.spacing.height', 'Height Spacing')}</Text>
<NumberInput
value={parameters.heightSpacer}
onChange={(value) => onParameterChange('heightSpacer', value || 50)}
min={0}
max={200}
disabled={disabled}
/>
</Stack>
</Stack>
);
};
export default WatermarkStyleSettings;
@@ -0,0 +1,46 @@
import React from "react";
import { Stack, Text, Select, ColorInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
import { alphabetOptions } from "../../../constants/addWatermarkConstants";
interface WatermarkTextStyleProps {
parameters: AddWatermarkParameters;
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
disabled?: boolean;
}
const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }: WatermarkTextStyleProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm">
<Stack gap="xs">
<Text size="xs" fw={500}>
{t("watermark.settings.color", "Colour")}
</Text>
<ColorInput
value={parameters.customColor}
onChange={(value) => onParameterChange("customColor", value)}
disabled={disabled}
format="hex"
/>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={500}>
{t("watermark.settings.alphabet", "Alphabet")}
</Text>
<Select
value={parameters.alphabet}
onChange={(value) => value && onParameterChange("alphabet", value)}
data={alphabetOptions}
disabled={disabled}
/>
</Stack>
</Stack>
);
};
export default WatermarkTextStyle;
@@ -0,0 +1,44 @@
import React from "react";
import { Button, Stack, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
interface WatermarkTypeSettingsProps {
watermarkType?: 'text' | 'image';
onWatermarkTypeChange: (type: 'text' | 'image') => void;
disabled?: boolean;
}
const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm">
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={watermarkType === 'text' ? 'filled' : 'outline'}
color={watermarkType === 'text' ? 'blue' : 'gray'}
onClick={() => onWatermarkTypeChange('text')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('watermark.watermarkType.text', 'Text')}
</div>
</Button>
<Button
variant={watermarkType === 'image' ? 'filled' : 'outline'}
color={watermarkType === 'image' ? 'blue' : 'gray'}
onClick={() => onWatermarkTypeChange('image')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('watermark.watermarkType.image', 'Image')}
</div>
</Button>
</div>
</Stack>
);
};
export default WatermarkTypeSettings;
@@ -0,0 +1,34 @@
import React from "react";
import { Stack, Text, TextInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
import { removeEmojis } from "../../../utils/textUtils";
interface WatermarkWordingProps {
parameters: AddWatermarkParameters;
onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
disabled?: boolean;
}
const WatermarkWording = ({ parameters, onParameterChange, disabled = false }: WatermarkWordingProps) => {
const { t } = useTranslation();
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const filteredValue = removeEmojis(value);
onParameterChange('watermarkText', filteredValue);
};
return (
<Stack gap="sm">
<TextInput
placeholder={t('watermark.settings.text.placeholder', 'Enter watermark text')}
value={parameters.watermarkText}
onChange={handleTextChange}
disabled={disabled}
/>
</Stack>
);
};
export default WatermarkWording;
@@ -0,0 +1,57 @@
import React, { useState, useEffect } from "react";
import { Stack, Text, NumberInput } from "@mantine/core";
interface NumberInputWithUnitProps {
label: string;
value: number;
onChange: (value: number | string) => void;
unit: string;
min?: number;
max?: number;
disabled?: boolean;
}
const NumberInputWithUnit = ({
label,
value,
onChange,
unit,
min,
max,
disabled = false
}: NumberInputWithUnitProps) => {
const [localValue, setLocalValue] = useState<number | string>(value);
// Sync local value when external value changes
useEffect(() => {
setLocalValue(value);
}, [value]);
const handleBlur = () => {
onChange(localValue);
};
return (
<Stack gap="xs" style={{ flex: 1 }}>
<Text size="xs" fw={500} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{label}
</Text>
<NumberInput
value={localValue}
onChange={setLocalValue}
onBlur={handleBlur}
min={min}
max={max}
disabled={disabled}
rightSection={
<Text size="sm" c="dimmed" pr="sm">
{unit}
</Text>
}
rightSectionWidth={unit.length * 8 + 20} // Dynamic width based on unit length
/>
</Stack>
);
};
export default NumberInputWithUnit;
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { Button, Stack, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import DownloadIcon from '@mui/icons-material/Download';
@@ -14,24 +14,32 @@ export interface ReviewToolStepProps<TParams = unknown> {
onFileClick?: (file: File) => void;
}
export function createReviewToolStep<TParams = unknown>(
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
props: ReviewToolStepProps<TParams>
): React.ReactElement {
function ReviewStepContent<TParams = unknown>({ operation, onFileClick }: { operation: ToolOperationHook<TParams>; onFileClick?: (file: File) => void }) {
const { t } = useTranslation();
const { operation } = props;
const stepRef = useRef<HTMLDivElement>(null);
const previewFiles = operation.files?.map((file, index) => ({
file,
thumbnail: operation.thumbnails[index]
})) || [];
return createStep(t("review", "Review"), {
isVisible: props.isVisible,
_excludeFromCount: true,
_noPadding: true
}, (
<Stack gap="sm" >
// Auto-scroll to bottom when content appears
useEffect(() => {
if (stepRef.current && (previewFiles.length > 0 || operation.downloadUrl || operation.errorMessage)) {
const scrollableContainer = stepRef.current.closest('[style*="overflow: auto"]') as HTMLElement;
if (scrollableContainer) {
setTimeout(() => {
scrollableContainer.scrollTo({
top: scrollableContainer.scrollHeight,
behavior: 'smooth'
});
}, 100); // Small delay to ensure content is rendered
}
}
}, [previewFiles.length, operation.downloadUrl, operation.errorMessage]);
return (
<Stack gap="sm" ref={stepRef}>
<ErrorNotification
error={operation.errorMessage}
onClose={operation.clearError}
@@ -40,7 +48,7 @@ export function createReviewToolStep<TParams = unknown>(
{previewFiles.length > 0 && (
<ResultsPreview
files={previewFiles}
onFileClick={props.onFileClick}
onFileClick={onFileClick}
isGeneratingThumbnails={operation.isGeneratingThumbnails}
/>
)}
@@ -61,5 +69,23 @@ export function createReviewToolStep<TParams = unknown>(
<SuggestedToolsSection />
</Stack>
);
}
export function createReviewToolStep<TParams = unknown>(
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
props: ReviewToolStepProps<TParams>
): React.ReactElement {
const { t } = useTranslation();
return createStep(t("review", "Review"), {
isVisible: props.isVisible,
_excludeFromCount: true,
_noPadding: true
}, (
<ReviewStepContent
operation={props.operation}
onFileClick={props.onFileClick}
/>
));
}
@@ -3,12 +3,13 @@ import { Text, Stack, Box, Flex, Divider } from '@mantine/core';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { Tooltip } from '../../shared/Tooltip';
import { TooltipTip } from '../../shared/tooltip/TooltipContent';
import { TooltipTip } from '../../../types/tips';
import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep';
import { createReviewToolStep, ReviewToolStepProps } from './ReviewToolStep';
interface ToolStepContextType {
visibleStepCount: number;
forceStepNumbers?: boolean;
}
const ToolStepContext = createContext<ToolStepContextType | null>(null);
@@ -82,10 +83,11 @@ const ToolStep = ({
const parent = useContext(ToolStepContext);
// Auto-detect if we should show numbers based on sibling count
// Auto-detect if we should show numbers based on sibling count or force option
const shouldShowNumber = useMemo(() => {
if (showNumber !== undefined) return showNumber;
return parent ? parent.visibleStepCount >= 3 : false;
if (showNumber !== undefined) return showNumber; // Individual step override
if (parent?.forceStepNumbers) return true; // Flow-level force
return parent ? parent.visibleStepCount >= 3 : false; // Auto-detect
}, [showNumber, parent]);
const stepNumber = _stepNumber;
@@ -196,7 +198,7 @@ export function createToolSteps() {
}
// Context provider wrapper for tools using the factory
export function ToolStepProvider({ children }: { children: React.ReactNode }) {
export function ToolStepProvider({ children, forceStepNumbers }: { children: React.ReactNode; forceStepNumbers?: boolean }) {
// Count visible steps from children that are ToolStep elements
const visibleStepCount = useMemo(() => {
let count = 0;
@@ -212,8 +214,9 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) {
}, [children]);
const contextValue = useMemo(() => ({
visibleStepCount
}), [visibleStepCount]);
visibleStepCount,
forceStepNumbers
}), [visibleStepCount, forceStepNumbers]);
return (
<ToolStepContext.Provider value={contextValue}>
@@ -49,6 +49,7 @@ export interface ToolFlowConfig {
steps: MiddleStepConfig[];
executeButton?: ExecuteButtonConfig;
review: ReviewStepConfig;
forceStepNumbers?: boolean;
}
/**
@@ -60,7 +61,7 @@ export function createToolFlow(config: ToolFlowConfig) {
return (
<Stack gap="sm" p="sm" h="95vh" w="100%" style={{ overflow: 'auto' }}>
<ToolStepProvider>
<ToolStepProvider forceStepNumbers={config.forceStepNumbers}>
{/* Files Step */}
{steps.createFilesStep({
selectedFiles: config.files.selectedFiles,
@@ -0,0 +1,176 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent, TooltipTip } from '../../types/tips';
// Shared tooltip content to reduce duplication
const useSharedWatermarkContent = () => {
const { t } = useTranslation();
const languageSupportTip: TooltipTip = {
title: t("watermark.tooltip.language.title", "Language Support"),
description: t("watermark.tooltip.language.text", "Choose the appropriate language setting to ensure proper font rendering for your text.")
};
const appearanceTip: TooltipTip = {
title: t("watermark.tooltip.appearance.title", "Appearance Settings"),
description: t("watermark.tooltip.appearance.text", "Control how your watermark looks and blends with the document."),
bullets: [
t("watermark.tooltip.appearance.bullet1", "Rotation: -360° to 360° for angled watermarks"),
t("watermark.tooltip.appearance.bullet2", "Opacity: 0-100% for transparency control"),
t("watermark.tooltip.appearance.bullet3", "Lower opacity creates subtle watermarks")
]
};
const spacingTip: TooltipTip = {
title: t("watermark.tooltip.spacing.title", "Spacing Control"),
description: t("watermark.tooltip.spacing.text", "Adjust the spacing between repeated watermarks across the page."),
bullets: [
t("watermark.tooltip.spacing.bullet1", "Width spacing: Horizontal distance between watermarks"),
t("watermark.tooltip.spacing.bullet2", "Height spacing: Vertical distance between watermarks"),
t("watermark.tooltip.spacing.bullet3", "Higher values create more spread out patterns")
]
};
return { languageSupportTip, appearanceTip, spacingTip };
};
export const useWatermarkTypeTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("watermark.tooltip.type.header.title", "Watermark Type Selection")
},
tips: [
{
title: t("watermark.tooltip.type.description.title", "Choose Your Watermark"),
description: t("watermark.tooltip.type.description.text", "Select between text or image watermarks based on your needs.")
},
{
title: t("watermark.tooltip.type.text.title", "Text Watermarks"),
description: t("watermark.tooltip.type.text.text", "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors."),
bullets: [
t("watermark.tooltip.type.text.bullet1", "Customizable fonts and languages"),
t("watermark.tooltip.type.text.bullet2", "Adjustable colors and transparency"),
t("watermark.tooltip.type.text.bullet3", "Ideal for legal or branding text")
]
},
{
title: t("watermark.tooltip.type.image.title", "Image Watermarks"),
description: t("watermark.tooltip.type.image.text", "Use logos, stamps, or any image as a watermark. Great for branding and visual identification."),
bullets: [
t("watermark.tooltip.type.image.bullet1", "Upload any image format"),
t("watermark.tooltip.type.image.bullet2", "Maintains image quality"),
t("watermark.tooltip.type.image.bullet3", "Perfect for logos and stamps")
]
}
]
};
};
export const useWatermarkWordingTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("watermark.tooltip.wording.header.title", "Text Content")
},
tips: [
{
title: t("watermark.tooltip.wording.text.title", "Watermark Text"),
description: t("watermark.tooltip.wording.text.text", "Enter the text that will appear as your watermark across the document."),
bullets: [
t("watermark.tooltip.wording.text.bullet1", "Keep it concise for better readability"),
t("watermark.tooltip.wording.text.bullet2", "Common examples: 'CONFIDENTIAL', 'DRAFT', company name"),
t("watermark.tooltip.wording.text.bullet3", "Emoji characters are not supported and will be filtered out")
]
}
]
};
};
export const useWatermarkTextStyleTips = (): TooltipContent => {
const { t } = useTranslation();
const { languageSupportTip } = useSharedWatermarkContent();
return {
header: {
title: t("watermark.tooltip.textStyle.header.title", "Text Style")
},
tips: [
{
title: t("watermark.tooltip.textStyle.color.title", "Color Selection"),
description: t("watermark.tooltip.textStyle.color.text", "Choose a color that provides good contrast with your document content."),
bullets: [
t("watermark.tooltip.textStyle.color.bullet1", "Light gray (#d3d3d3) for subtle watermarks"),
t("watermark.tooltip.textStyle.color.bullet2", "Black or dark colors for high contrast"),
t("watermark.tooltip.textStyle.color.bullet3", "Custom colors for branding purposes")
]
},
languageSupportTip
]
};
};
export const useWatermarkFileTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("watermark.tooltip.file.header.title", "Image Upload")
},
tips: [
{
title: t("watermark.tooltip.file.upload.title", "Image Selection"),
description: t("watermark.tooltip.file.upload.text", "Upload an image file to use as your watermark."),
bullets: [
t("watermark.tooltip.file.upload.bullet1", "Supports common formats: PNG, JPG, GIF, BMP"),
t("watermark.tooltip.file.upload.bullet2", "PNG with transparency works best"),
t("watermark.tooltip.file.upload.bullet3", "Higher resolution images maintain quality better")
]
},
{
title: t("watermark.tooltip.file.recommendations.title", "Best Practices"),
description: t("watermark.tooltip.file.recommendations.text", "Tips for optimal image watermark results."),
bullets: [
t("watermark.tooltip.file.recommendations.bullet1", "Use logos or stamps with transparent backgrounds"),
t("watermark.tooltip.file.recommendations.bullet2", "Simple designs work better than complex images"),
t("watermark.tooltip.file.recommendations.bullet3", "Consider the final document size when choosing resolution")
]
}
]
};
};
export const useWatermarkFormattingTips = (): TooltipContent => {
const { t } = useTranslation();
const { appearanceTip, spacingTip } = useSharedWatermarkContent();
return {
header: {
title: t("watermark.tooltip.formatting.header.title", "Formatting & Layout")
},
tips: [
{
title: t("watermark.tooltip.formatting.size.title", "Size Control"),
description: t("watermark.tooltip.formatting.size.text", "Adjust the size of your watermark (text or image)."),
bullets: [
t("watermark.tooltip.formatting.size.bullet1", "Larger sizes create more prominent watermarks")
]
},
appearanceTip,
spacingTip,
{
title: t("watermark.tooltip.formatting.security.title", "Security Option"),
description: t("watermark.tooltip.formatting.security.text", "Convert the final PDF to an image-based format for enhanced security."),
bullets: [
t("watermark.tooltip.formatting.security.bullet1", "Prevents text selection and copying"),
t("watermark.tooltip.formatting.security.bullet2", "Makes watermarks harder to remove"),
t("watermark.tooltip.formatting.security.bullet3", "Results in larger file sizes"),
t("watermark.tooltip.formatting.security.bullet4", "Best for sensitive or copyrighted content")
]
}
]
};
};